From d9bac1cabdc6ea372339d26131546ad6ec5c874e Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 27 Dec 2024 01:33:15 -0800 Subject: [PATCH 1/3] feat: first pass at a single session command This adds a shellexec command for launching connections that only require one remote session. --- pkg/shellexec/shellexec.go | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index a6fcf00e4d..0cd88983fd 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "html/template" "io" "log" "os" @@ -237,6 +238,77 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil } +var singleSessionCmdTemplate = `bash -c ' \ +{{.wshPath}} connserver --single || \ +( uname -m && \ + read -r count && + for ((i=0; i {{.tempPath}} && \ + chmod a+x {{.tempPath}} && \ + mv {{.tempPath}} {{.wshPath}} && + {{.wshPath}} connserver --single \ +) +` + +func StartSingleSessionRemoteShellProc(termSize waveobj.TermSize, conn *conncontroller.SSHConn) (*ShellProc, error) { + client := conn.GetClient() + session, err := client.NewSession() + if err != nil { + return nil, err + } + remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe() + if err != nil { + return nil, err + } + + remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe() + if err != nil { + return nil, err + } + + pipePty := &PipePty{ + remoteStdinWrite: remoteStdinWriteOurs, + remoteStdoutRead: remoteStdoutReadOurs, + } + if termSize.Rows == 0 || termSize.Cols == 0 { + termSize.Rows = shellutil.DefaultTermRows + termSize.Cols = shellutil.DefaultTermCols + } + if termSize.Rows <= 0 || termSize.Cols <= 0 { + return nil, fmt.Errorf("invalid term size: %v", termSize) + } + session.Stdin = remoteStdinRead + session.Stdout = remoteStdoutWrite + session.Stderr = remoteStdoutWrite + + session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) + + wshDir := "~/.waveterm/bin" + wshPath := wshDir + "/wsh" + var installWords = map[string]string{ + "wshPath": wshPath, + "tempPath": wshPath + ".temp", + } + + //todo add code that allows streaming base64 for download + + singleSessionCmd := &bytes.Buffer{} + cmdTemplate := template.Must(template.New("").Parse(singleSessionCmdTemplate)) + cmdTemplate.Execute(singleSessionCmd, installWords) + + log.Printf("full single session command is: %s", singleSessionCmd) + sessionWrap := MakeSessionWrap(session, singleSessionCmd.String(), pipePty) + + err = sessionWrap.Start() + if err != nil { + pipePty.Close() + return nil, err + } + return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil +} + func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() session, err := client.NewSession() From 7880eb8f89ea247f45d48256b1fdcaf7e436ccfb Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 27 Dec 2024 01:50:27 -0800 Subject: [PATCH 2/3] feat: add blockcontroller outline for single shell This adds the code that will call the shellexec code. It also disables the existing connserver/install code for single shell mode. --- frontend/types/gotypes.d.ts | 1 + pkg/blockcontroller/blockcontroller.go | 37 +++++++++++++++++++++ pkg/remote/conncontroller/conncontroller.go | 4 ++- pkg/wshrpc/wshrpctypes.go | 1 + 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 0e37807612..4ad2981c44 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -291,6 +291,7 @@ declare global { "conn:wshenabled"?: boolean; "conn:askbeforewshinstall"?: boolean; "conn:overrideconfig"?: boolean; + "conn:singlesession"?: boolean; "display:hidden"?: boolean; "display:order"?: number; "term:*"?: boolean; diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 35d32eb680..798e7eee24 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -316,6 +316,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj } else { return fmt.Errorf("unknown controller type %q", bc.ControllerType) } + + var singleSession bool + fullConfig := wconfig.ReadFullConfig() + existingConnection, ok := fullConfig.Connections[remoteName] + if ok { + singleSession = existingConnection.ConnSingleSession + } var shellProc *shellexec.ShellProc if strings.HasPrefix(remoteName, "wsl://") { wslName := strings.TrimPrefix(remoteName, "wsl://") @@ -340,6 +347,36 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj if err != nil { return err } + } else if remoteName != "" && singleSession { + credentialCtx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFunc() + + opts, err := remote.ParseOpts(remoteName) + if err != nil { + return err + } + conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{}) + connStatus := conn.DeriveConnStatus() + if connStatus.Status != conncontroller.Status_Connected { + return fmt.Errorf("not connected, cannot start shellproc") + } + if !blockMeta.GetBool(waveobj.MetaKey_CmdNoWsh, false) { + jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.GetDomainSocketName()) + if err != nil { + return fmt.Errorf("error making jwt token: %w", err) + } + cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr + } + shellProc, err = shellexec.StartSingleSessionRemoteShellProc(rc.TermSize, conn) + if err != nil { + return err + } + // todo + // i have disabled the conn server for this type of connection + // this means we need to receive a signal from the process once it has + // opened a domain socket. then, once that is done, we can forward the + // unix domain socket here. also, we need to set the wsh boolean true as + // is done in the current connserver implementation } else if remoteName != "" { credentialCtx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second) defer cancelFunc() diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index ec20245f17..4904a0e6b9 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -511,6 +511,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn }) config := wconfig.ReadFullConfig() enableWsh := config.Settings.ConnWshEnabled + var singleSession bool askBeforeInstall := config.Settings.ConnAskBeforeWshInstall connSettings, ok := config.Connections[conn.GetName()] if ok { @@ -520,8 +521,9 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn if connSettings.ConnAskBeforeWshInstall != nil { askBeforeInstall = *connSettings.ConnAskBeforeWshInstall } + singleSession = connSettings.ConnSingleSession } - if enableWsh { + if enableWsh && !singleSession { installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) if errors.Is(installErr, &WshInstallSkipError{}) { // skips are not true errors diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index ef72c867d2..a8c9c3309b 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -462,6 +462,7 @@ type ConnKeywords struct { ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"` ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"` + ConnSingleSession bool `json:"conn:singlesession,omitempty"` DisplayHidden *bool `json:"display:hidden,omitempty"` DisplayOrder float32 `json:"display:order,omitempty"` From ce7d6dd520a532f6f5b294524ff52940c5bf6813 Mon Sep 17 00:00:00 2001 From: Sylvia Crowe Date: Fri, 3 Jan 2025 10:17:41 -0800 Subject: [PATCH 3/3] fix: change return type in singleSession code --- pkg/blockcontroller/blockcontroller.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index edb1ca7f7a..bde4450e61 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -360,23 +360,23 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta opts, err := remote.ParseOpts(remoteName) if err != nil { - return err + return nil, err } conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{}) connStatus := conn.DeriveConnStatus() if connStatus.Status != conncontroller.Status_Connected { - return fmt.Errorf("not connected, cannot start shellproc") + return nil, fmt.Errorf("not connected, cannot start shellproc") } if !blockMeta.GetBool(waveobj.MetaKey_CmdNoWsh, false) { jwtStr, err := wshutil.MakeClientJWTToken(wshrpc.RpcContext{TabId: bc.TabId, BlockId: bc.BlockId, Conn: conn.Opts.String()}, conn.GetDomainSocketName()) if err != nil { - return fmt.Errorf("error making jwt token: %w", err) + return nil, fmt.Errorf("error making jwt token: %w", err) } cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr } shellProc, err = shellexec.StartSingleSessionRemoteShellProc(rc.TermSize, conn) if err != nil { - return err + return nil, err } // todo // i have disabled the conn server for this type of connection