diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index ddf73c1669..8b6b2f9764 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 c5afeb5c8f..bde4450e61 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -323,6 +323,13 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta } else { return nil, 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://") @@ -347,6 +354,36 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta if err != nil { return nil, 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 nil, err + } + conn := conncontroller.GetConn(credentialCtx, opts, false, &wshrpc.ConnKeywords{}) + connStatus := conn.DeriveConnStatus() + if connStatus.Status != conncontroller.Status_Connected { + 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 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 nil, 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 0428ff1d4b..2ced2ed949 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -512,6 +512,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 { @@ -521,8 +522,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/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 6f2145827c..b9c8ba7808 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "html/template" "io" "log" "os" @@ -241,6 +242,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() 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"`