From 15c01d2444b79f1d347ab547b4b3b247b801665f Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 3 Jan 2025 14:21:01 -0800 Subject: [PATCH 01/16] remove detectshell and pwsh special case. wrap with sh -c to get fish/pwsh support. --- pkg/remote/conncontroller/conncontroller.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 0428ff1d4b..2e774d7d2b 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -189,7 +189,7 @@ func (conn *SSHConn) OpenDomainSocketListener() error { return fmt.Errorf("error generating random string: %w", err) } sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr) - log.Printf("remote domain socket %s %q\n", conn.GetName(), conn.GetDomainSocketName()) + log.Printf("remote domain socket %s %q\n", conn.GetName(), sockName) listener, err := client.ListenUnix(sockName) if err != nil { return fmt.Errorf("unable to request connection domain socket: %v", err) @@ -241,18 +241,10 @@ func (conn *SSHConn) StartConnServer() error { pipeRead, pipeWrite := io.Pipe() sshSession.Stdout = pipeWrite sshSession.Stderr = pipeWrite - shellPath, err := remote.DetectShell(client) - if err != nil { - return err - } - var cmdStr string - if remote.IsPowershell(shellPath) { - cmdStr = fmt.Sprintf("$env:%s=\"%s\"; %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath) - } else { - cmdStr = fmt.Sprintf("%s=\"%s\" %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath) - } + cmdStr := fmt.Sprintf("%s=\"%s\" %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath) log.Printf("starting conn controller: %s\n", cmdStr) - err = sshSession.Start(cmdStr) + shWrappedCmdStr := fmt.Sprintf("sh -c %s", genconn.HardQuote(cmdStr)) + err = sshSession.Start(shWrappedCmdStr) if err != nil { return fmt.Errorf("unable to start conn controller: %w", err) } @@ -523,6 +515,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn } } if enableWsh { + installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) if errors.Is(installErr, &WshInstallSkipError{}) { // skips are not true errors From 83ebae8666a71ad95c7b3f8bdd366253a9e53980 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 7 Jan 2025 11:14:05 -0800 Subject: [PATCH 02/16] use wconfig.GetWatcher().GetFullConfig() instead of re-reading the config --- pkg/remote/conncontroller/conncontroller.go | 10 +++++++--- pkg/remote/sshclient.go | 2 +- pkg/wconfig/settingsconfig.go | 2 ++ pkg/wsl/wsl.go | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 2e774d7d2b..5ad4ddad6f 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -457,7 +457,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords // logic for saving connection and potential flags (we only save once a connection has been made successfully) // at the moment, identity files is the only saved flag var identityFiles []string - existingConfig := wconfig.ReadFullConfig() + existingConfig := wconfig.GetWatcher().GetFullConfig() existingConnection, ok := existingConfig.Connections[conn.GetName()] if ok { identityFiles = existingConnection.SshIdentityFile @@ -491,6 +491,10 @@ func (conn *SSHConn) WithLock(fn func()) { fn() } +func (conn *SSHConn) requiresWshUserConfirmation() bool { + return true +} + func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { @@ -502,7 +506,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn conn.WithLock(func() { conn.Client = client }) - config := wconfig.ReadFullConfig() + config := wconfig.GetWatcher().GetFullConfig() enableWsh := config.Settings.ConnWshEnabled askBeforeInstall := config.Settings.ConnAskBeforeWshInstall connSettings, ok := config.Connections[conn.GetName()] @@ -728,7 +732,7 @@ func GetConnectionsList() ([]string, error) { func GetConnectionsFromInternalConfig() []string { var internalNames []string - config := wconfig.ReadFullConfig() + config := wconfig.GetWatcher().GetFullConfig() for internalName := range config.Connections { if strings.HasPrefix(internalName, "wsl://") { // don't add wsl conns to this list diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index b06ad2b8e6..b19fe558d7 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -660,7 +660,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. } rawName := opts.String() - fullConfig := wconfig.ReadFullConfig() + fullConfig := wconfig.GetWatcher().GetFullConfig() internalSshConfigKeywords, ok := fullConfig.Connections[rawName] if !ok { internalSshConfigKeywords = wshrpc.ConnKeywords{} diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 8d1afef542..cfdcc7719b 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -306,6 +306,8 @@ func readConfigPart(partName string, simpleMerge bool) (waveobj.MetaMapType, []C return mergeMetaMap(rtn, homeConfigs, simpleMerge), allErrs } +// this function should only be called by the wconfig code. +// in golang code, the best way to get the current config is via the watcher -- wconfig.GetWatcher().GetFullConfig() func ReadFullConfig() FullConfigType { var fullConfig FullConfigType configRType := reflect.TypeOf(fullConfig) diff --git a/pkg/wsl/wsl.go b/pkg/wsl/wsl.go index 8ca9e4f1d4..f9b5a6e1e9 100644 --- a/pkg/wsl/wsl.go +++ b/pkg/wsl/wsl.go @@ -446,7 +446,7 @@ func (conn *WslConn) connectInternal(ctx context.Context) error { if err != nil { return err } - config := wconfig.ReadFullConfig() + config := wconfig.GetWatcher().GetFullConfig() installErr := conn.CheckAndInstallWsh(ctx, conn.GetName(), &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall}) if installErr != nil { return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) From 45f5a7e85e210e53bebfcc6b8406cc5591fec5a1 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 7 Jan 2025 11:27:31 -0800 Subject: [PATCH 03/16] switch 'askbeforewshinstall' to a bool ptr. resolve with a new helper fn --- pkg/remote/conncontroller/conncontroller.go | 31 +++++++++++---------- pkg/wconfig/settingsconfig.go | 13 +++++++-- pkg/wsl/wsl.go | 3 +- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index d8fab92d94..d045eac095 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -491,8 +491,21 @@ func (conn *SSHConn) WithLock(fn func()) { fn() } -func (conn *SSHConn) requiresWshUserConfirmation() bool { - return true +// returns (enable-wsh, ask-before-install) +func (conn *SSHConn) getConnWshSettings() (bool, bool) { + config := wconfig.GetWatcher().GetFullConfig() + enableWsh := config.Settings.ConnWshEnabled + askBeforeInstall := wconfig.DefaultBoolPtr(config.Settings.ConnAskBeforeWshInstall, true) + connSettings, ok := config.Connections[conn.GetName()] + if ok { + if connSettings.ConnWshEnabled != nil { + enableWsh = *connSettings.ConnWshEnabled + } + if connSettings.ConnAskBeforeWshInstall != nil { + askBeforeInstall = *connSettings.ConnAskBeforeWshInstall + } + } + return enableWsh, askBeforeInstall } func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { @@ -506,20 +519,8 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn conn.WithLock(func() { conn.Client = client }) - config := wconfig.GetWatcher().GetFullConfig() - enableWsh := config.Settings.ConnWshEnabled - askBeforeInstall := config.Settings.ConnAskBeforeWshInstall - connSettings, ok := config.Connections[conn.GetName()] - if ok { - if connSettings.ConnWshEnabled != nil { - enableWsh = *connSettings.ConnWshEnabled - } - if connSettings.ConnAskBeforeWshInstall != nil { - askBeforeInstall = *connSettings.ConnAskBeforeWshInstall - } - } + enableWsh, askBeforeInstall := conn.getConnWshSettings() if enableWsh { - installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) if errors.Is(installErr, &WshInstallSkipError{}) { // skips are not true errors diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 7a236e324f..3ab3cf37d4 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -115,9 +115,9 @@ type SettingsType struct { TelemetryClear bool `json:"telemetry:*,omitempty"` TelemetryEnabled bool `json:"telemetry:enabled,omitempty"` - ConnClear bool `json:"conn:*,omitempty"` - ConnAskBeforeWshInstall bool `json:"conn:askbeforewshinstall,omitempty"` - ConnWshEnabled bool `json:"conn:wshenabled,omitempty"` + ConnClear bool `json:"conn:*,omitempty"` + ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` + ConnWshEnabled bool `json:"conn:wshenabled,omitempty"` } type ConfigError struct { @@ -136,6 +136,13 @@ type FullConfigType struct { ConfigErrors []ConfigError `json:"configerrors" configfile:"-"` } +func DefaultBoolPtr(arg *bool, def bool) bool { + if arg == nil { + return def + } + return *arg +} + func goBackWS(barr []byte, offset int) int { if offset >= len(barr) { offset = offset - 1 diff --git a/pkg/wsl/wsl.go b/pkg/wsl/wsl.go index 708232cde5..47c6b3cf1a 100644 --- a/pkg/wsl/wsl.go +++ b/pkg/wsl/wsl.go @@ -447,7 +447,8 @@ func (conn *WslConn) connectInternal(ctx context.Context) error { return err } config := wconfig.GetWatcher().GetFullConfig() - installErr := conn.CheckAndInstallWsh(ctx, conn.GetName(), &WshInstallOpts{NoUserPrompt: !config.Settings.ConnAskBeforeWshInstall}) + wshAsk := wconfig.DefaultBoolPtr(config.Settings.ConnAskBeforeWshInstall, true) + installErr := conn.CheckAndInstallWsh(ctx, conn.GetName(), &WshInstallOpts{NoUserPrompt: !wshAsk}) if installErr != nil { return fmt.Errorf("conncontroller %s wsh install error: %v", conn.GetName(), installErr) } From 6b44c34ff33030de2b2e22c47b9c11412d316238 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 7 Jan 2025 23:00:44 -0800 Subject: [PATCH 04/16] checkpoint on conncontroller refactor --- pkg/remote/conncontroller/conncontroller.go | 208 ++++++++++++++++---- pkg/wshutil/wshrpcio.go | 34 ++++ 2 files changed, 206 insertions(+), 36 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index d045eac095..b29a57771b 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -56,7 +56,7 @@ type SSHConn struct { WshEnabled *atomic.Bool Opts *remote.SSHOpts Client *ssh.Client - SockName string + DomainSockName string // if "", then no domain socket DomainSockListener net.Listener ConnController *ssh.Session Error string @@ -66,6 +66,12 @@ type SSHConn struct { ActiveConnNum int } +var ConnServerCmdTemplate = strings.TrimSpace(` +%s version || echo "not-installed" +read jwt_token +WAVETERM_JWT="$jwt_token" %s connserver +`) + func GetAllConnStatus() []wshrpc.ConnStatus { globalLock.Lock() defer globalLock.Unlock() @@ -143,6 +149,7 @@ func (conn *SSHConn) close_nolock() { if conn.DomainSockListener != nil { conn.DomainSockListener.Close() conn.DomainSockListener = nil + conn.DomainSockName = "" } if conn.ConnController != nil { conn.ConnController.Close() @@ -157,7 +164,7 @@ func (conn *SSHConn) close_nolock() { func (conn *SSHConn) GetDomainSocketName() string { conn.Lock.Lock() defer conn.Lock.Unlock() - return conn.SockName + return conn.DomainSockName } func (conn *SSHConn) GetStatus() string { @@ -172,13 +179,8 @@ func (conn *SSHConn) GetName() string { } func (conn *SSHConn) OpenDomainSocketListener() error { - var allowed bool - conn.WithLock(func() { - if conn.Status != Status_Connecting { - allowed = false - } else { - allowed = true - } + allowed := WithLockRtn(conn, func() bool { + return conn.Status == Status_Connecting }) if !allowed { return fmt.Errorf("cannot open domain socket for %q when status is %q", conn.GetName(), conn.GetStatus()) @@ -195,7 +197,7 @@ func (conn *SSHConn) OpenDomainSocketListener() error { return fmt.Errorf("unable to request connection domain socket: %v", err) } conn.WithLock(func() { - conn.SockName = sockName + conn.DomainSockName = sockName conn.DomainSockListener = listener }) go func() { @@ -204,24 +206,38 @@ func (conn *SSHConn) OpenDomainSocketListener() error { }() defer conn.WithLock(func() { conn.DomainSockListener = nil - conn.SockName = "" + conn.DomainSockName = "" }) wshutil.RunWshRpcOverListener(listener) }() return nil } -func (conn *SSHConn) StartConnServer() error { - var allowed bool - conn.WithLock(func() { - if conn.Status != Status_Connecting { - allowed = false - } else { - allowed = true - } +// expects the output of `wsh version` which looks like `wsh v0.10.4` or "not-installed" +func isWshVersionUpToDate(wshVersionLine string) (bool, error) { + wshVersionLine = strings.TrimSpace(wshVersionLine) + if wshVersionLine == "not-installed" { + return false, nil + } + parts := strings.Fields(wshVersionLine) + if len(parts) != 2 { + return false, fmt.Errorf("unexpected version format: %s", wshVersionLine) + } + clientVersion := parts[1] + expectedVersion := fmt.Sprintf("v%s", wavebase.WaveVersion) + if semver.Compare(clientVersion, expectedVersion) < 0 { + return false, nil + } + return true, nil +} + +// returns (needsInstall, error) +func (conn *SSHConn) StartConnServer() (bool, error) { + allowed := WithLockRtn(conn, func() bool { + return conn.Status == Status_Connecting }) if !allowed { - return fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus()) + return false, fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus()) } client := conn.GetClient() wshPath := remote.GetWshPath(client) @@ -232,21 +248,46 @@ func (conn *SSHConn) StartConnServer() error { sockName := conn.GetDomainSocketName() jwtToken, err := wshutil.MakeClientJWTToken(rpcCtx, sockName) if err != nil { - return fmt.Errorf("unable to create jwt token for conn controller: %w", err) + return false, fmt.Errorf("unable to create jwt token for conn controller: %w", err) } sshSession, err := client.NewSession() if err != nil { - return fmt.Errorf("unable to create ssh session for conn controller: %w", err) + return false, fmt.Errorf("unable to create ssh session for conn controller: %w", err) } pipeRead, pipeWrite := io.Pipe() sshSession.Stdout = pipeWrite sshSession.Stderr = pipeWrite - cmdStr := fmt.Sprintf("%s=\"%s\" %s connserver", wshutil.WaveJwtTokenVarName, jwtToken, wshPath) + stdinPipe, err := sshSession.StdinPipe() + if err != nil { + return false, fmt.Errorf("unable to get stdin pipe: %w", err) + } + cmdStr := fmt.Sprintf(ConnServerCmdTemplate, wshPath, wshPath) log.Printf("starting conn controller: %s\n", cmdStr) shWrappedCmdStr := fmt.Sprintf("sh -c %s", genconn.HardQuote(cmdStr)) err = sshSession.Start(shWrappedCmdStr) if err != nil { - return fmt.Errorf("unable to start conn controller: %w", err) + return false, fmt.Errorf("unable to start conn controller command: %w", err) + } + linesChan := wshutil.StreamToLinesChan(pipeRead) + versionLine, err := wshutil.ReadLineWithTimeout(linesChan, 2*time.Second) + if err != nil { + sshSession.Close() + return false, fmt.Errorf("error reading wsh version: %w", err) + } + isUpToDate, err := isWshVersionUpToDate(versionLine) + if err != nil { + sshSession.Close() + return false, fmt.Errorf("error checking wsh version: %w", err) + } + if !isUpToDate { + sshSession.Close() + return true, nil + } + // write the jwt + _, err = fmt.Fprintf(stdinPipe, "%s\n", jwtToken) + if err != nil { + sshSession.Close() + return false, fmt.Errorf("failed to write JWT token: %w", err) } conn.WithLock(func() { conn.ConnController = sshSession @@ -267,25 +308,26 @@ func (conn *SSHConn) StartConnServer() error { defer func() { panichandler.PanicHandler("conncontroller:sshSession-output", recover()) }() - readErr := wshutil.StreamToLines(pipeRead, func(line []byte) { - lineStr := string(line) - if !strings.HasSuffix(lineStr, "\n") { - lineStr += "\n" + for output := range linesChan { + if output.Error != nil { + log.Printf("[conncontroller:%s:output] error: %v\n", conn.GetName(), output.Error) + continue } - log.Printf("[conncontroller:%s:output] %s", conn.GetName(), lineStr) - }) - if readErr != nil && readErr != io.EOF { - log.Printf("[conncontroller:%s] error reading output: %v\n", conn.GetName(), readErr) + line := output.Line + if !strings.HasSuffix(line, "\n") { + line += "\n" + } + log.Printf("[conncontroller:%s:output] %s", conn.GetName(), line) } }() regCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn)) if err != nil { - return fmt.Errorf("timeout waiting for connserver to register") + return false, fmt.Errorf("timeout waiting for connserver to register") } time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready") - return nil + return false, nil } type WshInstallOpts struct { @@ -299,6 +341,53 @@ func (wise *WshInstallSkipError) Error() string { return "skipping wsh installation" } +var queryTextTemplate = strings.TrimSpace(` +Wave requires Wave Shell Extensions to be +installed on %q +to ensure a seamless experience. + +Would you like to install them? +`) + +// returns (allowed, error) +func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDisplayName string) (bool, error) { + queryText := fmt.Sprintf(queryTextTemplate, clientDisplayName) + title := "Install Wave Shell Extensions" + request := &userinput.UserInputRequest{ + ResponseType: "confirm", + QueryText: queryText, + Title: title, + Markdown: true, + CheckBoxMsg: "Automatically install for all connections", + OkLabel: "Install wsh", + CancelLabel: "No wsh", + } + response, err := userinput.GetUserInput(ctx, request) + if err != nil { + return false, err + } + meta := make(map[string]any) + meta["conn:wshenabled"] = response.Confirm + err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta) + if err != nil { + log.Printf("warning: error writing to connections file: %v", err) + } + if !response.Confirm { + return false, nil + } + if response.CheckboxStat { + meta := waveobj.MetaMapType{ + wconfig.ConfigKey_ConnAskBeforeWshInstall: false, + } + setConfigErr := wconfig.SetBaseConfigValue(meta) + if setConfigErr != nil { + // this is not a critical error, just log and continue + log.Printf("warning: error writing to base config file: %v", err) + } + } + return true, nil +} + func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { if opts == nil { opts = &WshInstallOpts{} @@ -491,6 +580,12 @@ func (conn *SSHConn) WithLock(fn func()) { fn() } +func WithLockRtn[T any](conn *SSHConn, fn func() T) T { + conn.Lock.Lock() + defer conn.Lock.Unlock() + return fn() +} + // returns (enable-wsh, ask-before-install) func (conn *SSHConn) getConnWshSettings() (bool, bool) { config := wconfig.GetWatcher().GetFullConfig() @@ -501,13 +596,54 @@ func (conn *SSHConn) getConnWshSettings() (bool, bool) { if connSettings.ConnWshEnabled != nil { enableWsh = *connSettings.ConnWshEnabled } - if connSettings.ConnAskBeforeWshInstall != nil { + // if the connection object exists, and conn:askbeforewshinstall is not set, the user must have allowed it + // TODO: in v0.12+ this should be removed. we'll explicitly write a "false" into the connection object on successful connection + if connSettings.ConnAskBeforeWshInstall == nil { + askBeforeInstall = false + } else { askBeforeInstall = *connSettings.ConnAskBeforeWshInstall } } return enableWsh, askBeforeInstall } +func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, error) { + enableWsh, askBeforeInstall := conn.getConnWshSettings() + if !enableWsh { + return false, nil + } + if askBeforeInstall { + allowInstall, err := conn.getPermissionToInstallWsh(ctx, clientDisplayName) + if err != nil { + log.Printf("error getting permission to install wsh: %v\n", err) + return false, err + } + if !allowInstall { + return false, nil + } + } + // TODO make sure installed + err := conn.OpenDomainSocketListener() + if err != nil { + return false, fmt.Errorf("error opening domain socket listener: %w", err) + } + needsInstall, err := conn.StartConnServer() + if err != nil { + return false, fmt.Errorf("error starting conn server: %w", err) + } + if needsInstall { + // TODO: install conn server + needsInstall, err = conn.StartConnServer() + if err != nil { + return false, fmt.Errorf("error starting conn server (after install): %w", err) + } + if needsInstall { + return false, fmt.Errorf("conn server not installed correctly (after install)") + } + } + return true, nil +} + func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { @@ -544,7 +680,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn if dsErr != nil { log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), dsErr) } else { - csErr = conn.StartConnServer() + _, csErr = conn.StartConnServer() if csErr != nil { log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) } diff --git a/pkg/wshutil/wshrpcio.go b/pkg/wshutil/wshrpcio.go index ca0a617c90..38b9fd47eb 100644 --- a/pkg/wshutil/wshrpcio.go +++ b/pkg/wshutil/wshrpcio.go @@ -5,8 +5,10 @@ package wshutil import ( "bytes" + "context" "fmt" "io" + "time" ) // special I/O wrappers for wshrpc @@ -55,6 +57,38 @@ func StreamToLines(input io.Reader, lineFn func([]byte)) error { } } +type LineOutput struct { + Line string + Error error +} + +// starts a goroutine to drive the channel +func StreamToLinesChan(input io.Reader) chan LineOutput { + ch := make(chan LineOutput) + go func() { + defer close(ch) + err := StreamToLines(input, func(line []byte) { + ch <- LineOutput{Line: string(line)} + }) + if err != nil && err != io.EOF { + ch <- LineOutput{Error: err} + } + }() + return ch +} + +func ReadLineWithTimeout(ch chan LineOutput, timeout time.Duration) (string, error) { + select { + case output := <-ch: + if output.Error != nil { + return "", output.Error + } + return output.Line, nil + case <-time.After(timeout): + return "", context.DeadlineExceeded + } +} + func AdaptStreamToMsgCh(input io.Reader, output chan []byte) error { return StreamToLines(input, func(line []byte) { output <- line From 119e1582778cc6d2b4c153f1fcc559dd55f8a1a9 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Jan 2025 09:59:55 -0800 Subject: [PATCH 05/16] change ensureconnection to take a logblockid, pass through into conn code using a special context --- cmd/wsh/cmd/wshcmd-conn.go | 6 +++++- frontend/app/block/blockframe.tsx | 12 ++++++++++-- frontend/app/store/wshclientapi.ts | 2 +- frontend/app/view/preview/preview.tsx | 2 +- frontend/types/gotypes.d.ts | 6 ++++++ pkg/remote/conncontroller/conncontroller.go | 5 +++++ pkg/remote/connutil.go | 13 +++++++++++++ pkg/wshrpc/wshclient/wshclient.go | 2 +- pkg/wshrpc/wshrpctypes.go | 7 ++++++- pkg/wshrpc/wshserver/wshserver.go | 11 +++++++---- 10 files changed, 55 insertions(+), 11 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-conn.go b/cmd/wsh/cmd/wshcmd-conn.go index 4f4b083eda..74fc91415a 100644 --- a/cmd/wsh/cmd/wshcmd-conn.go +++ b/cmd/wsh/cmd/wshcmd-conn.go @@ -186,7 +186,11 @@ func connEnsureRun(cmd *cobra.Command, args []string) error { if err := validateConnectionName(connName); err != nil { return err } - err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) + data := wshrpc.ConnEnsureData{ + ConnName: connName, + LogBlockId: RpcContext.BlockId, + } + err := wshclient.ConnEnsureCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000}) if err != nil { return fmt.Errorf("ensuring connection: %w", err) } diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index e487221fdf..b078157d3b 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -541,7 +541,11 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => { const connName = blockData?.meta?.connection; if (!util.isBlank(connName)) { console.log("ensure conn", nodeModel.blockId, connName); - RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }).catch((e) => { + RpcApi.ConnEnsureCommand( + TabRpcClient, + { connname: connName, logblockid: nodeModel.blockId }, + { timeout: 60000 } + ).catch((e) => { console.log("error ensuring connection", nodeModel.blockId, connName, e); }); } @@ -691,7 +695,11 @@ const ChangeConnectionBlockModal = React.memo( meta: { connection: connName, file: newCwd }, }); try { - await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }); + await RpcApi.ConnEnsureCommand( + TabRpcClient, + { connname: connName, logblockid: blockId }, + { timeout: 60000 } + ); } catch (e) { console.log("error connecting", blockId, connName, e); } diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index c1e3e23ac5..eda69bf9ac 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -38,7 +38,7 @@ class RpcApiType { } // command "connensure" [call] - ConnEnsureCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + ConnEnsureCommand(client: WshClient, data: ConnEnsureData, opts?: RpcOpts): Promise { return client.wshRpcCall("connensure", data, opts); } diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 1a56ac12b1..ece27f24fc 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -364,7 +364,7 @@ export class PreviewModel implements ViewModel { this.connection = atom>(async (get) => { const connName = get(this.blockAtom)?.meta?.connection; try { - await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }); + await RpcApi.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 }); globalStore.set(this.connectionError, ""); } catch (e) { globalStore.set(this.connectionError, e as string); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 4354af5cdc..4dbe116513 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -286,6 +286,12 @@ declare global { metamaptype: MetaType; }; + // wshrpc.ConnEnsureData + type ConnEnsureData = { + connname: string; + logblockid?: string; + }; + // wshrpc.ConnKeywords type ConnKeywords = { "conn:wshenabled"?: boolean; diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index b29a57771b..d72b5bb88b 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -111,6 +111,11 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { } } +func (conn *SSHConn) Logf(debugLevel int, format string, args ...interface{}) { + logStr := fmt.Sprintf("[debug%d] ", debugLevel) + fmt.Sprintf(format, args...) + log.Printf("[conncontroller:%s] %s", conn.GetName(), logStr) +} + func (conn *SSHConn) FireConnChangeEvent() { status := conn.DeriveConnStatus() event := wps.WaveEvent{ diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index 7c8a1dff1a..9bc57b825d 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -25,6 +25,10 @@ import ( var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`) +type logBlockIdContextKeyType struct{} + +var logBlockIdContextKey = logBlockIdContextKeyType{} + func ParseOpts(input string) (*SSHOpts, error) { m := userHostRe.FindStringSubmatch(input) if m == nil { @@ -36,6 +40,15 @@ func ParseOpts(input string) (*SSHOpts, error) { return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil } +func ContextWithLogBlockId(ctx context.Context, blockId string) context.Context { + return context.WithValue(ctx, logBlockIdContextKey, blockId) +} + +func GetLogBlockIdFromContext(ctx context.Context) string { + blockId, _ := ctx.Value(logBlockIdContextKey).(string) + return blockId +} + func DetectShell(client *ssh.Client) (string, error) { wshPath := GetWshPath(client) diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index e365ca68f0..a46fc65b14 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -50,7 +50,7 @@ func ConnDisconnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) } // command "connensure", wshserver.ConnEnsureCommand -func ConnEnsureCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { +func ConnEnsureCommand(w *wshutil.WshRpc, data wshrpc.ConnEnsureData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connensure", data, opts) return err } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index d28adf5c85..f38064cfb2 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -154,7 +154,7 @@ type WshRpcInterface interface { // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) WslStatusCommand(ctx context.Context) ([]ConnStatus, error) - ConnEnsureCommand(ctx context.Context, connName string) error + ConnEnsureCommand(ctx context.Context, data ConnEnsureData) error ConnReinstallWshCommand(ctx context.Context, connName string) error ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error ConnDisconnectCommand(ctx context.Context, connName string) error @@ -646,3 +646,8 @@ type ActivityUpdate struct { WshCmds map[string]int `json:"wshcmds,omitempty"` Conn map[string]int `json:"conn,omitempty"` } + +type ConnEnsureData struct { + ConnName string `json:"connname"` + LogBlockId string `json:"logblockid,omitempty"` +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 0154c12333..a9f39ec89d 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -596,12 +596,15 @@ func (ws *WshServer) WslStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, return rtn, nil } -func (ws *WshServer) ConnEnsureCommand(ctx context.Context, connName string) error { - if strings.HasPrefix(connName, "wsl://") { - distroName := strings.TrimPrefix(connName, "wsl://") +func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsureData) error { + if data.LogBlockId != "" { + ctx = remote.ContextWithLogBlockId(ctx, data.LogBlockId) + } + if strings.HasPrefix(data.ConnName, "wsl://") { + distroName := strings.TrimPrefix(data.ConnName, "wsl://") return wsl.EnsureConnection(ctx, distroName) } - return conncontroller.EnsureConnection(ctx, connName) + return conncontroller.EnsureConnection(ctx, data.ConnName) } func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error { From c0dab83d6367ceae64d28f82983cedfc3148d392 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Jan 2025 10:59:49 -0800 Subject: [PATCH 06/16] blocklogger. hook up to connect command, control with term meta --- frontend/app/store/wshclientapi.ts | 5 +++ frontend/app/view/term/term.tsx | 28 +++++++++++++ frontend/types/gotypes.d.ts | 7 ++++ pkg/blocklogger/blocklogger.go | 45 +++++++++++++++++++++ pkg/remote/conncontroller/conncontroller.go | 8 ++-- pkg/remote/connutil.go | 13 ------ pkg/waveobj/metaconsts.go | 1 + pkg/waveobj/wtypemeta.go | 1 + pkg/wshrpc/wshclient/wshclient.go | 6 +++ pkg/wshrpc/wshrpctypes.go | 6 +++ pkg/wshrpc/wshserver/wshserver.go | 22 +++++++++- 11 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 pkg/blocklogger/blocklogger.go diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index eda69bf9ac..80adb40fbd 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -57,6 +57,11 @@ class RpcApiType { return client.wshRpcCall("connstatus", null, opts); } + // command "controllerappendoutput" [call] + ControllerAppendOutputCommand(client: WshClient, data: CommandControllerAppendOutputData, opts?: RpcOpts): Promise { + return client.wshRpcCall("controllerappendoutput", data, opts); + } + // command "controllerinput" [call] ControllerInputCommand(client: WshClient, data: CommandBlockInputData, opts?: RpcOpts): Promise { return client.wshRpcCall("controllerinput", data, opts); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index f79cd8c6c3..3320742017 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -682,6 +682,34 @@ class TermViewModel implements ViewModel { }, }); } + const debugConn = blockData?.meta?.["term:debugconn"]; + fullMenu.push({ + label: "Debug Connection", + submenu: [ + { + label: "On", + type: "checkbox", + checked: debugConn, + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:debugconn": true }, + }); + }, + }, + { + label: "Off", + type: "checkbox", + checked: !debugConn, + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:debugconn": null }, + }); + }, + }, + ], + }); return fullMenu; } } diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 4dbe116513..8cd0cdd199 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -125,6 +125,12 @@ declare global { view: string; }; + // wshrpc.CommandControllerAppendOutputData + type CommandControllerAppendOutputData = { + blockid: string; + data64: string; + }; + // wshrpc.CommandControllerResyncData type CommandControllerResyncData = { forcerestart?: boolean; @@ -500,6 +506,7 @@ declare global { "term:vdomtoolbarblockid"?: string; "term:transparency"?: number; "term:allowbracketedpaste"?: boolean; + "term:debugconn"?: boolean; "web:zoom"?: number; "web:hidenav"?: boolean; "markdown:fontsize"?: number; diff --git a/pkg/blocklogger/blocklogger.go b/pkg/blocklogger/blocklogger.go new file mode 100644 index 0000000000..bb3d7b9234 --- /dev/null +++ b/pkg/blocklogger/blocklogger.go @@ -0,0 +1,45 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package blocklogger + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +type logBlockIdContextKeyType struct{} + +var logBlockIdContextKey = logBlockIdContextKeyType{} + +func ContextWithLogBlockId(ctx context.Context, blockId string) context.Context { + return context.WithValue(ctx, logBlockIdContextKey, blockId) +} + +func GetLogBlockIdFromContext(ctx context.Context) string { + if ctx == nil { + return "" + } + blockId, _ := ctx.Value(logBlockIdContextKey).(string) + return blockId +} + +func Logf(ctx context.Context, format string, args ...interface{}) { + logBlockId := GetLogBlockIdFromContext(ctx) + if logBlockId == "" { + return + } + logStr := fmt.Sprintf(format, args...) + logStr = strings.ReplaceAll(logStr, "\n", "\r\n") + client := wshclient.GetBareRpcClient() + data := wshrpc.CommandControllerAppendOutputData{ + BlockId: logBlockId, + Data64: base64.StdEncoding.EncodeToString([]byte(logStr)), + } + wshclient.ControllerAppendOutputCommand(client, data, &wshrpc.RpcOpts{NoResponse: true}) +} diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index d72b5bb88b..52f0894c43 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -20,6 +20,7 @@ import ( "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" + "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote" @@ -111,11 +112,6 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { } } -func (conn *SSHConn) Logf(debugLevel int, format string, args ...interface{}) { - logStr := fmt.Sprintf("[debug%d] ", debugLevel) + fmt.Sprintf(format, args...) - log.Printf("[conncontroller:%s] %s", conn.GetName(), logStr) -} - func (conn *SSHConn) FireConnChangeEvent() { status := conn.DeriveConnStatus() event := wps.WaveEvent{ @@ -508,6 +504,7 @@ func (conn *SSHConn) WaitForConnect(ctx context.Context) error { // does not return an error since that error is stored inside of SSHConn func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { + blocklogger.Logf(ctx, "\n") var connectAllowed bool conn.WithLock(func() { if conn.Status == Status_Connecting || conn.Status == Status_Connected { @@ -650,6 +647,7 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { + blocklogger.Logf(ctx, "[conndebug] connecting to %s\n", conn.GetName()) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index 9bc57b825d..7c8a1dff1a 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -25,10 +25,6 @@ import ( var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`) -type logBlockIdContextKeyType struct{} - -var logBlockIdContextKey = logBlockIdContextKeyType{} - func ParseOpts(input string) (*SSHOpts, error) { m := userHostRe.FindStringSubmatch(input) if m == nil { @@ -40,15 +36,6 @@ func ParseOpts(input string) (*SSHOpts, error) { return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil } -func ContextWithLogBlockId(ctx context.Context, blockId string) context.Context { - return context.WithValue(ctx, logBlockIdContextKey, blockId) -} - -func GetLogBlockIdFromContext(ctx context.Context) string { - blockId, _ := ctx.Value(logBlockIdContextKey).(string) - return blockId -} - func DetectShell(client *ssh.Client) (string, error) { wshPath := GetWshPath(client) diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 9e3a81ec97..292322abed 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -95,6 +95,7 @@ const ( MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid" MetaKey_TermTransparency = "term:transparency" MetaKey_TermAllowBracketedPaste = "term:allowbracketedpaste" + MetaKey_TermDebugConn = "term:debugconn" MetaKey_WebZoom = "web:zoom" MetaKey_WebHideNav = "web:hidenav" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 49aa5a2593..f997650e03 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -96,6 +96,7 @@ type MetaTSType struct { TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5 TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"` + TermDebugConn bool `json:"term:debugconn,omitempty"` WebZoom float64 `json:"web:zoom,omitempty"` WebHideNav *bool `json:"web:hidenav,omitempty"` diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index a46fc65b14..d1a1d10444 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -73,6 +73,12 @@ func ConnStatusCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]wshrpc.ConnSt return resp, err } +// command "controllerappendoutput", wshserver.ControllerAppendOutputCommand +func ControllerAppendOutputCommand(w *wshutil.WshRpc, data wshrpc.CommandControllerAppendOutputData, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "controllerappendoutput", data, opts) + return err +} + // command "controllerinput", wshserver.ControllerInputCommand func ControllerInputCommand(w *wshutil.WshRpc, data wshrpc.CommandBlockInputData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "controllerinput", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index f38064cfb2..4b55f6369a 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -118,6 +118,7 @@ type WshRpcInterface interface { ControllerInputCommand(ctx context.Context, data CommandBlockInputData) error ControllerStopCommand(ctx context.Context, blockId string) error ControllerResyncCommand(ctx context.Context, data CommandControllerResyncData) error + ControllerAppendOutputCommand(ctx context.Context, data CommandControllerAppendOutputData) error ResolveIdsCommand(ctx context.Context, data CommandResolveIdsData) (CommandResolveIdsRtnData, error) CreateBlockCommand(ctx context.Context, data CommandCreateBlockData) (waveobj.ORef, error) CreateSubBlockCommand(ctx context.Context, data CommandCreateSubBlockData) (waveobj.ORef, error) @@ -311,6 +312,11 @@ type CommandControllerResyncData struct { RtOpts *waveobj.RuntimeOpts `json:"rtopts,omitempty"` } +type CommandControllerAppendOutputData struct { + BlockId string `json:"blockid"` + Data64 string `json:"data64"` +} + type CommandBlockInputData struct { BlockId string `json:"blockid" wshcontext:"BlockId"` InputData64 string `json:"inputdata64,omitempty"` diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index a9f39ec89d..82dd55c858 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -19,6 +19,7 @@ import ( "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/panichandler" "github.com/wavetermdev/waveterm/pkg/remote" @@ -260,6 +261,19 @@ func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.Com 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 { @@ -598,7 +612,13 @@ func (ws *WshServer) WslStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsureData) error { if data.LogBlockId != "" { - ctx = remote.ContextWithLogBlockId(ctx, data.LogBlockId) + block, err := wstore.DBMustGet[*waveobj.Block](ctx, data.LogBlockId) + if err == nil { + connDebug := block.Meta.GetBool(waveobj.MetaKey_TermDebugConn, false) + if connDebug { + ctx = blocklogger.ContextWithLogBlockId(ctx, data.LogBlockId) + } + } } if strings.HasPrefix(data.ConnName, "wsl://") { distroName := strings.TrimPrefix(data.ConnName, "wsl://") From 860607ce3d3a86997b9a57e8606a4455785edcdc Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 8 Jan 2025 12:08:10 -0800 Subject: [PATCH 07/16] info/debug logs for blocklogger. start logging much more connection info --- frontend/app/view/term/term.tsx | 23 +++++-- frontend/types/gotypes.d.ts | 2 +- pkg/blocklogger/blocklogger.go | 44 +++++++++---- pkg/remote/conncontroller/conncontroller.go | 69 ++++++++++++++++----- pkg/waveobj/wtypemeta.go | 2 +- pkg/wshrpc/wshserver/wshserver.go | 6 +- 6 files changed, 107 insertions(+), 39 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 3320742017..b357606965 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -687,24 +687,35 @@ class TermViewModel implements ViewModel { label: "Debug Connection", submenu: [ { - label: "On", + label: "Off", type: "checkbox", - checked: debugConn, + checked: !debugConn, click: () => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), - meta: { "term:debugconn": true }, + meta: { "term:debugconn": null }, }); }, }, { - label: "Off", + label: "Info", type: "checkbox", - checked: !debugConn, + checked: debugConn == "info", click: () => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), - meta: { "term:debugconn": null }, + meta: { "term:debugconn": "info" }, + }); + }, + }, + { + label: "Verbose", + type: "checkbox", + checked: debugConn == "debug", + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:debugconn": "debug" }, }); }, }, diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 8cd0cdd199..41e2a67ab2 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -506,7 +506,7 @@ declare global { "term:vdomtoolbarblockid"?: string; "term:transparency"?: number; "term:allowbracketedpaste"?: boolean; - "term:debugconn"?: boolean; + "term:debugconn"?: string; "web:zoom"?: number; "web:hidenav"?: boolean; "markdown:fontsize"?: number; diff --git a/pkg/blocklogger/blocklogger.go b/pkg/blocklogger/blocklogger.go index bb3d7b9234..e017223a93 100644 --- a/pkg/blocklogger/blocklogger.go +++ b/pkg/blocklogger/blocklogger.go @@ -17,29 +17,49 @@ type logBlockIdContextKeyType struct{} var logBlockIdContextKey = logBlockIdContextKeyType{} -func ContextWithLogBlockId(ctx context.Context, blockId string) context.Context { - return context.WithValue(ctx, logBlockIdContextKey, blockId) +type logBlockIdData struct { + BlockId string + Verbose bool } -func GetLogBlockIdFromContext(ctx context.Context) string { +func ContextWithLogBlockId(ctx context.Context, blockId string, verbose bool) context.Context { + return context.WithValue(ctx, logBlockIdContextKey, &logBlockIdData{BlockId: blockId, Verbose: verbose}) +} + +func getLogBlockData(ctx context.Context) *logBlockIdData { if ctx == nil { - return "" + return nil } - blockId, _ := ctx.Value(logBlockIdContextKey).(string) - return blockId + dataPtr := ctx.Value(logBlockIdContextKey) + if dataPtr == nil { + return nil + } + return dataPtr.(*logBlockIdData) } -func Logf(ctx context.Context, format string, args ...interface{}) { - logBlockId := GetLogBlockIdFromContext(ctx) - if logBlockId == "" { - return - } +func writeLogf(blockId string, format string, args []any) { logStr := fmt.Sprintf(format, args...) logStr = strings.ReplaceAll(logStr, "\n", "\r\n") client := wshclient.GetBareRpcClient() data := wshrpc.CommandControllerAppendOutputData{ - BlockId: logBlockId, + BlockId: blockId, Data64: base64.StdEncoding.EncodeToString([]byte(logStr)), } wshclient.ControllerAppendOutputCommand(client, data, &wshrpc.RpcOpts{NoResponse: true}) } + +func Infof(ctx context.Context, format string, args ...any) { + logData := getLogBlockData(ctx) + if logData == nil { + return + } + writeLogf(logData.BlockId, format, args) +} + +func Debugf(ctx context.Context, format string, args ...interface{}) { + logData := getLogBlockData(ctx) + if logData == nil || !logData.Verbose { + return + } + writeLogf(logData.BlockId, format, args) +} diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 52f0894c43..ec39d0fe50 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -112,6 +112,11 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { } } +func (conn *SSHConn) Infof(ctx context.Context, format string, args ...any) { + log.Print(fmt.Sprintf("[conn:%s] ", conn.GetName()) + fmt.Sprintf(format, args...)) + blocklogger.Infof(ctx, "[conndebug] "+format, args...) +} + func (conn *SSHConn) FireConnChangeEvent() { status := conn.DeriveConnStatus() event := wps.WaveEvent{ @@ -179,7 +184,8 @@ func (conn *SSHConn) GetName() string { return conn.Opts.String() } -func (conn *SSHConn) OpenDomainSocketListener() error { +func (conn *SSHConn) OpenDomainSocketListener(ctx context.Context) error { + conn.Infof(ctx, "running OpenDomainSocketListener...\n") allowed := WithLockRtn(conn, func() bool { return conn.Status == Status_Connecting }) @@ -192,7 +198,7 @@ func (conn *SSHConn) OpenDomainSocketListener() error { return fmt.Errorf("error generating random string: %w", err) } sockName := fmt.Sprintf("/tmp/waveterm-%s.sock", randStr) - log.Printf("remote domain socket %s %q\n", conn.GetName(), sockName) + conn.Infof(ctx, "generated domain socket name %s\n", sockName) listener, err := client.ListenUnix(sockName) if err != nil { return fmt.Errorf("unable to request connection domain socket: %v", err) @@ -201,6 +207,7 @@ func (conn *SSHConn) OpenDomainSocketListener() error { conn.DomainSockName = sockName conn.DomainSockListener = listener }) + conn.Infof(ctx, "successfully connected domain socket\n") go func() { defer func() { panichandler.PanicHandler("conncontroller:OpenDomainSocketListener", recover()) @@ -233,7 +240,8 @@ func isWshVersionUpToDate(wshVersionLine string) (bool, error) { } // returns (needsInstall, error) -func (conn *SSHConn) StartConnServer() (bool, error) { +func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { + conn.Infof(ctx, "running StartConnServer...\n") allowed := WithLockRtn(conn, func() bool { return conn.Status == Status_Connecting }) @@ -352,6 +360,7 @@ Would you like to install them? // returns (allowed, error) func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDisplayName string) (bool, error) { + conn.Infof(ctx, "running getPermissionToInstallWsh...\n") queryText := fmt.Sprintf(queryTextTemplate, clientDisplayName) title := "Install Wave Shell Extensions" request := &userinput.UserInputRequest{ @@ -363,12 +372,16 @@ func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDispla OkLabel: "Install wsh", CancelLabel: "No wsh", } + conn.Infof(ctx, "requesting user confirmation...\n") response, err := userinput.GetUserInput(ctx, request) if err != nil { + conn.Infof(ctx, "error getting user input: %v\n", err) return false, err } + conn.Infof(ctx, "user response to allowing wsh: %v\n", response.Confirm) meta := make(map[string]any) meta["conn:wshenabled"] = response.Confirm + conn.Infof(ctx, "writing conn:wshenabled=%v to connections.json\n", response.Confirm) err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta) if err != nil { log.Printf("warning: error writing to connections file: %v", err) @@ -377,6 +390,7 @@ func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDispla return false, nil } if response.CheckboxStat { + conn.Infof(ctx, "writing conn:askbeforewshinstall=false to settings.json\n") meta := waveobj.MetaMapType{ wconfig.ConfigKey_ConnAskBeforeWshInstall: false, } @@ -451,16 +465,27 @@ func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s } } } - log.Printf("attempting to install wsh to `%s`", clientDisplayName) - clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(client)) + err = conn.installWsh(ctx) + if err != nil { + return err + } + return nil +} + +func (conn *SSHConn) installWsh(ctx context.Context) error { + conn.Infof(ctx, "running installWsh...\n") + clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(conn.GetClient())) if err != nil { + conn.Infof(ctx, "ERROR detecting client platform: %v\n", err) return err } - err = remote.CpWshToRemote(ctx, client, clientOs, clientArch) + conn.Infof(ctx, "detected remote platform os:%s arch:%s\n", clientOs, clientArch) + err = remote.CpWshToRemote(ctx, conn.GetClient(), clientOs, clientArch) if err != nil { - return fmt.Errorf("error installing wsh to remote: %w", err) + conn.Infof(ctx, "ERROR copying wsh binary to remote: %v\n", err) + return fmt.Errorf("error copying wsh binary to remote: %w", err) } - log.Printf("successfully installed wsh on %s\n", conn.GetName()) + conn.Infof(ctx, "successfully installed wsh\n") return nil } @@ -504,7 +529,7 @@ func (conn *SSHConn) WaitForConnect(ctx context.Context) error { // does not return an error since that error is stored inside of SSHConn func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { - blocklogger.Logf(ctx, "\n") + blocklogger.Infof(ctx, "\n") var connectAllowed bool conn.WithLock(func() { if conn.Status == Status_Connecting || conn.Status == Status_Connected { @@ -515,14 +540,16 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords connectAllowed = true } }) - log.Printf("Connect %s\n", conn.GetName()) if !connectAllowed { + conn.Infof(ctx, "cannot connect to when status is %q\n", conn.GetStatus()) return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus()) } + conn.Infof(ctx, "trying to connect to %q...\n", conn.GetName()) conn.FireConnChangeEvent() err := conn.connectInternal(ctx, connFlags) conn.WithLock(func() { if err != nil { + conn.Infof(ctx, "ERROR %v\n\n", err) conn.Status = Status_Error conn.Error = err.Error() conn.close_nolock() @@ -530,6 +557,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords Conn: map[string]int{"ssh:connecterror": 1}, }, "ssh-connconnect") } else { + conn.Infof(ctx, "successfully connected\n\n") conn.Status = Status_Connected conn.LastConnectTime = time.Now().UnixMilli() if conn.ActiveConnNum == 0 { @@ -610,7 +638,9 @@ func (conn *SSHConn) getConnWshSettings() (bool, bool) { } func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, error) { + conn.Infof(ctx, "running tryEnableWsh...") enableWsh, askBeforeInstall := conn.getConnWshSettings() + conn.Infof(ctx, "wsh settings enable:%v ask:%v", enableWsh, askBeforeInstall) if !enableWsh { return false, nil } @@ -625,21 +655,26 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } } // TODO make sure installed - err := conn.OpenDomainSocketListener() + err := conn.OpenDomainSocketListener(ctx) if err != nil { + conn.Infof(ctx, "ERROR opening domain socket listener: %v\n", err) return false, fmt.Errorf("error opening domain socket listener: %w", err) } - needsInstall, err := conn.StartConnServer() + needsInstall, err := conn.StartConnServer(ctx) if err != nil { + conn.Infof(ctx, "ERROR starting conn server: %v\n", err) return false, fmt.Errorf("error starting conn server: %w", err) } if needsInstall { + conn.Infof(ctx, "connserver needs to be (re)installed\n") // TODO: install conn server - needsInstall, err = conn.StartConnServer() + needsInstall, err = conn.StartConnServer(ctx) if err != nil { + conn.Infof(ctx, "ERROR starting conn server (after install): %v\n", err) return false, fmt.Errorf("error starting conn server (after install): %w", err) } if needsInstall { + conn.Infof(ctx, "conn server not installed correctly (after install)\n") return false, fmt.Errorf("conn server not installed correctly (after install)") } } @@ -647,18 +682,20 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { - blocklogger.Logf(ctx, "[conndebug] connecting to %s\n", conn.GetName()) + conn.Infof(ctx, "connectInternal %s\n", conn.GetName()) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) return err } fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) + conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) conn.WithLock(func() { conn.Client = client }) enableWsh, askBeforeInstall := conn.getConnWshSettings() + conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall) if enableWsh { installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) if errors.Is(installErr, &WshInstallSkipError{}) { @@ -678,12 +715,12 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn } if conn.WshEnabled.Load() { - dsErr := conn.OpenDomainSocketListener() + dsErr := conn.OpenDomainSocketListener(ctx) var csErr error if dsErr != nil { log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), dsErr) } else { - _, csErr = conn.StartConnServer() + _, csErr = conn.StartConnServer(ctx) if csErr != nil { log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) } diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index f997650e03..a4292abfbf 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -96,7 +96,7 @@ type MetaTSType struct { TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5 TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"` - TermDebugConn bool `json:"term:debugconn,omitempty"` + TermDebugConn string `json:"term:debugconn,omitempty"` // null, info, debug WebZoom float64 `json:"web:zoom,omitempty"` WebHideNav *bool `json:"web:hidenav,omitempty"` diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 82dd55c858..5b133300d4 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -614,9 +614,9 @@ func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsu if data.LogBlockId != "" { block, err := wstore.DBMustGet[*waveobj.Block](ctx, data.LogBlockId) if err == nil { - connDebug := block.Meta.GetBool(waveobj.MetaKey_TermDebugConn, false) - if connDebug { - ctx = blocklogger.ContextWithLogBlockId(ctx, data.LogBlockId) + connDebug := block.Meta.GetString(waveobj.MetaKey_TermDebugConn, "") + if connDebug != "" { + ctx = blocklogger.ContextWithLogBlockId(ctx, data.LogBlockId, connDebug == "debug") } } } From 599de9271ebdfcca16f13e1d2acf3ec4a3b0a1fa Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Jan 2025 17:38:50 -0800 Subject: [PATCH 08/16] force serialization of blocklogger events --- cmd/server/main-server.go | 2 ++ pkg/blocklogger/blocklogger.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index ae679805d5..a5adcd0514 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -17,6 +17,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/authkey" "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" @@ -297,6 +298,7 @@ func main() { go stdinReadWatch() go telemetryLoop() configWatcher() + blocklogger.InitBlockLogger() webListener, err := web.MakeTCPListener("web") if err != nil { log.Printf("error creating web listener: %v\n", err) diff --git a/pkg/blocklogger/blocklogger.go b/pkg/blocklogger/blocklogger.go index e017223a93..be3a1b43bf 100644 --- a/pkg/blocklogger/blocklogger.go +++ b/pkg/blocklogger/blocklogger.go @@ -13,6 +13,25 @@ import ( "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) +// Buffer size for the output channel +const outputBufferSize = 1000 + +var outputChan chan wshrpc.CommandControllerAppendOutputData + +func InitBlockLogger() { + outputChan = make(chan wshrpc.CommandControllerAppendOutputData, outputBufferSize) + // Start the output runner + go outputRunner() +} + +func outputRunner() { + client := wshclient.GetBareRpcClient() + for data := range outputChan { + // Process each output request synchronously, waiting for response + wshclient.ControllerAppendOutputCommand(client, data, nil) + } +} + type logBlockIdContextKeyType struct{} var logBlockIdContextKey = logBlockIdContextKeyType{} @@ -37,15 +56,21 @@ func getLogBlockData(ctx context.Context) *logBlockIdData { return dataPtr.(*logBlockIdData) } +func queueLogData(data wshrpc.CommandControllerAppendOutputData) { + select { + case outputChan <- data: + default: + } +} + func writeLogf(blockId string, format string, args []any) { logStr := fmt.Sprintf(format, args...) logStr = strings.ReplaceAll(logStr, "\n", "\r\n") - client := wshclient.GetBareRpcClient() data := wshrpc.CommandControllerAppendOutputData{ BlockId: blockId, Data64: base64.StdEncoding.EncodeToString([]byte(logStr)), } - wshclient.ControllerAppendOutputCommand(client, data, &wshrpc.RpcOpts{NoResponse: true}) + queueLogData(data) } func Infof(ctx context.Context, format string, args ...any) { From 48aa730be0a02fe04b6c64c9be8788a7661fd27e Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Jan 2025 17:39:26 -0800 Subject: [PATCH 09/16] more logging, use new wsh install flow --- pkg/remote/conncontroller/conncontroller.go | 97 +++++++++------------ 1 file changed, 39 insertions(+), 58 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index ec39d0fe50..423f6abf31 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -283,16 +283,19 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { sshSession.Close() return false, fmt.Errorf("error reading wsh version: %w", err) } + conn.Infof(ctx, "got connserver version: %s\n", strings.TrimSpace(versionLine)) isUpToDate, err := isWshVersionUpToDate(versionLine) if err != nil { sshSession.Close() return false, fmt.Errorf("error checking wsh version: %w", err) } + conn.Infof(ctx, "connserver update to date: %v\n", isUpToDate) if !isUpToDate { sshSession.Close() return true, nil } // write the jwt + conn.Infof(ctx, "writing jwt token to connserver\n") _, err = fmt.Fprintf(stdinPipe, "%s\n", jwtToken) if err != nil { sshSession.Close() @@ -329,6 +332,7 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { log.Printf("[conncontroller:%s:output] %s", conn.GetName(), line) } }() + conn.Infof(ctx, "connserver started, waiting for route to be registered\n") regCtx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn)) @@ -336,6 +340,7 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { return false, fmt.Errorf("timeout waiting for connserver to register") } time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready") + conn.Infof(ctx, "connserver is registered and ready\n") return false, nil } @@ -557,7 +562,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords Conn: map[string]int{"ssh:connecterror": 1}, }, "ssh-connconnect") } else { - conn.Infof(ctx, "successfully connected\n\n") + conn.Infof(ctx, "successfully connected (wsh:%v)\n\n", conn.WshEnabled.Load()) conn.Status = Status_Connected conn.LastConnectTime = time.Now().UnixMilli() if conn.ActiveConnNum == 0 { @@ -638,9 +643,9 @@ func (conn *SSHConn) getConnWshSettings() (bool, bool) { } func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, error) { - conn.Infof(ctx, "running tryEnableWsh...") + conn.Infof(ctx, "running tryEnableWsh...\n") enableWsh, askBeforeInstall := conn.getConnWshSettings() - conn.Infof(ctx, "wsh settings enable:%v ask:%v", enableWsh, askBeforeInstall) + conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall) if !enableWsh { return false, nil } @@ -667,7 +672,11 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } if needsInstall { conn.Infof(ctx, "connserver needs to be (re)installed\n") - // TODO: install conn server + err = conn.installWsh(ctx) + if err != nil { + conn.Infof(ctx, "ERROR installing wsh: %v\n", err) + return false, fmt.Errorf("error installing wsh: %w", err) + } needsInstall, err = conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server (after install): %v\n", err) @@ -681,6 +690,23 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) return true, nil } +func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool) { + conn.WshEnabled.Store(wshEnabled) + config := wconfig.GetWatcher().GetFullConfig() + connSettings, ok := config.Connections[conn.GetName()] + if ok && connSettings.ConnWshEnabled != nil { + return + } + meta := make(map[string]any) + meta["conn:wshenabled"] = wshEnabled + err := wconfig.SetConnectionsConfigValue(conn.GetName(), meta) + if err != nil { + conn.Infof(ctx, "WARN could not write conn:wshenabled=%v to connections.json: %v\n", wshEnabled, err) + log.Printf("warning: error writing to connections file: %v", err) + } + // doesn't return an error since none of this is required for connection to work +} + func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { conn.Infof(ctx, "connectInternal %s\n", conn.GetName()) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) @@ -688,64 +714,19 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) return err } - fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) - conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) - clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) conn.WithLock(func() { conn.Client = client }) - enableWsh, askBeforeInstall := conn.getConnWshSettings() - conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall) - if enableWsh { - installErr := conn.CheckAndInstallWsh(ctx, clientDisplayName, &WshInstallOpts{NoUserPrompt: !askBeforeInstall}) - if errors.Is(installErr, &WshInstallSkipError{}) { - // skips are not true errors - conn.WithLock(func() { - conn.WshEnabled.Store(false) - }) - } else if installErr != nil { - log.Printf("error: unable to install wsh shell extensions for %s: %v\n", conn.GetName(), err) - log.Print("attempting to run with nowsh instead") - conn.WithLock(func() { - conn.WshError = installErr.Error() - }) - conn.WshEnabled.Store(false) - } else { - conn.WshEnabled.Store(true) - } - - if conn.WshEnabled.Load() { - dsErr := conn.OpenDomainSocketListener(ctx) - var csErr error - if dsErr != nil { - log.Printf("error: unable to open domain socket listener for %s: %v\n", conn.GetName(), dsErr) - } else { - _, csErr = conn.StartConnServer(ctx) - if csErr != nil { - log.Printf("error: unable to start conn server for %s: %v\n", conn.GetName(), csErr) - } - } - if dsErr != nil || csErr != nil { - log.Print("attempting to run with nowsh instead") - var errmsgs []string - if dsErr != nil { - errmsgs = append(errmsgs, fmt.Sprintf("domain socket error: %s", dsErr.Error())) - } - if csErr != nil { - errmsgs = append(errmsgs, fmt.Sprintf("conn server error: %s", csErr.Error())) - } - combinedErr := fmt.Errorf("%s", strings.Join(errmsgs, " | ")) - conn.WithLock(func() { - conn.WshError = combinedErr.Error() - }) - conn.WshEnabled.Store(false) - } - } - } else { - conn.WshEnabled.Store(false) - } - conn.HasWaiter.Store(true) go conn.waitForDisconnect() + fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) + conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) + clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) + wshInstalled, enableErr := conn.tryEnableWsh(ctx, clientDisplayName) + if enableErr != nil { + conn.Infof(ctx, "ERROR enabling wsh: %v\n", enableErr) + conn.Infof(ctx, "will connect with wsh disabled\n") + } + conn.persistWshInstalled(ctx, wshInstalled) return nil } From f20b32985cea63cb28af4b60d33bfb90b2cf21a8 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Jan 2025 17:57:20 -0800 Subject: [PATCH 10/16] refocus after closing connection modal --- frontend/app/block/blockframe.tsx | 6 ++++-- frontend/app/store/keymodel.ts | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index b078157d3b..001f63bd79 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -23,10 +23,10 @@ import { getSettingsKeyAtom, getUserName, globalStore, - refocusNode, useBlockAtom, WOS, } from "@/app/store/global"; +import { globalRefocusWithTimeout } from "@/app/store/keymodel"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { ErrorBoundary } from "@/element/errorboundary"; @@ -887,12 +887,13 @@ const ChangeConnectionBlockModal = React.memo( } else { changeConnection(rowItem.value); globalStore.set(changeConnModalAtom, false); + globalRefocusWithTimeout(10); } } if (keyutil.checkKeyPressed(waveEvent, "Escape")) { globalStore.set(changeConnModalAtom, false); setConnSelected(""); - refocusNode(blockId); + globalRefocusWithTimeout(10); return true; } if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { @@ -924,6 +925,7 @@ const ChangeConnectionBlockModal = React.memo( onSelect={(selected: string) => { changeConnection(selected); globalStore.set(changeConnModalAtom, false); + globalRefocusWithTimeout(10); }} selectIndex={rowIndex} autoFocus={isNodeFocused} diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 59dd55d123..be9374e19b 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -146,6 +146,12 @@ function handleCmdI() { globalRefocus(); } +function globalRefocusWithTimeout(timeoutVal: number) { + setTimeout(() => { + globalRefocus(); + }, timeoutVal); +} + function globalRefocus() { const layoutModel = getLayoutModelForStaticTab(); const focusedNode = globalStore.get(layoutModel.focusedNode); @@ -403,6 +409,7 @@ export { getAllGlobalKeyBindings, getSimpleControlShiftAtom, globalRefocus, + globalRefocusWithTimeout, registerControlShiftStateUpdateHandler, registerElectronReinjectKeyHandler, registerGlobalKeys, From 26aad51c1ec0d7fd2e14ec328ba3fdb9e3971268 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Jan 2025 21:28:44 -0800 Subject: [PATCH 11/16] debugging info through remote sshclient. nowshreason. connect now also enables blocklogger. fix small remote display name in password prompt --- cmd/wsh/cmd/wshcmd-conn.go | 6 +- cmd/wsh/cmd/wshcmd-ssh.go | 3 +- frontend/app/block/blockframe.tsx | 8 ++- frontend/types/gotypes.d.ts | 2 + pkg/blockcontroller/blockcontroller.go | 4 +- pkg/blocklogger/blocklogger.go | 2 + pkg/remote/conncontroller/conncontroller.go | 74 +++++++++++++++------ pkg/remote/sshclient.go | 31 ++++++++- pkg/wshrpc/wshrpctypes.go | 6 +- pkg/wshrpc/wshserver/wshserver.go | 30 +++++---- 10 files changed, 123 insertions(+), 43 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-conn.go b/cmd/wsh/cmd/wshcmd-conn.go index 74fc91415a..344660bc60 100644 --- a/cmd/wsh/cmd/wshcmd-conn.go +++ b/cmd/wsh/cmd/wshcmd-conn.go @@ -173,7 +173,11 @@ func connConnectRun(cmd *cobra.Command, args []string) error { if err := validateConnectionName(connName); err != nil { return err } - err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000}) + data := wshrpc.ConnRequest{ + Host: connName, + LogBlockId: RpcContext.BlockId, + } + err := wshclient.ConnConnectCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000}) if err != nil { return fmt.Errorf("connecting connection: %w", err) } diff --git a/cmd/wsh/cmd/wshcmd-ssh.go b/cmd/wsh/cmd/wshcmd-ssh.go index f0686abe7c..ab5b552bb3 100644 --- a/cmd/wsh/cmd/wshcmd-ssh.go +++ b/cmd/wsh/cmd/wshcmd-ssh.go @@ -39,7 +39,8 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) { } // first, make a connection independent of the block connOpts := wshrpc.ConnRequest{ - Host: sshArg, + Host: sshArg, + LogBlockId: blockId, Keywords: wshrpc.ConnKeywords{ SshIdentityFile: identityFiles, }, diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 001f63bd79..e9f997e5ea 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -356,7 +356,11 @@ const ConnStatusOverlay = React.memo( }, [width, connStatus, setShowError]); const handleTryReconnect = React.useCallback(() => { - const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 }); + const prtn = RpcApi.ConnConnectCommand( + TabRpcClient, + { host: connName, logblockid: nodeModel.blockId }, + { timeout: 60000 } + ); prtn.catch((e) => console.log("error reconnecting", connName, e)); }, [connName]); @@ -764,7 +768,7 @@ const ChangeConnectionBlockModal = React.memo( onSelect: async (_: string) => { const prtn = RpcApi.ConnConnectCommand( TabRpcClient, - { host: connStatus.connection }, + { host: connStatus.connection, logblockid: blockId }, { timeout: 60000 } ); prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e)); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 79acb34074..f2cb60193a 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -329,6 +329,7 @@ declare global { type ConnRequest = { host: string; keywords?: ConnKeywords; + logblockid?: string; }; // wshrpc.ConnStatus @@ -341,6 +342,7 @@ declare global { activeconnnum: number; error?: string; wsherror?: string; + nowshreason?: string; }; // wshrpc.CpuDataRequest diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index bd74f15356..b87948a485 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -375,9 +375,7 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta } else { shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn) if err != nil { - conn.WithLock(func() { - conn.WshError = err.Error() - }) + conn.SetWshError(err) conn.WshEnabled.Store(false) log.Printf("error starting remote shell proc with wsh: %v", err) log.Print("attempting install without wsh") diff --git a/pkg/blocklogger/blocklogger.go b/pkg/blocklogger/blocklogger.go index be3a1b43bf..c7d6f79af0 100644 --- a/pkg/blocklogger/blocklogger.go +++ b/pkg/blocklogger/blocklogger.go @@ -7,6 +7,7 @@ import ( "context" "encoding/base64" "fmt" + "log" "strings" "github.com/wavetermdev/waveterm/pkg/wshrpc" @@ -25,6 +26,7 @@ func InitBlockLogger() { } func outputRunner() { + defer log.Printf("blocklogger: outputRunner exiting") client := wshclient.GetBareRpcClient() for data := range outputChan { // Process each output request synchronously, waiting for response diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 423f6abf31..d8b719332e 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -62,6 +62,7 @@ type SSHConn struct { ConnController *ssh.Session Error string WshError string + NoWshReason string HasWaiter *atomic.Bool LastConnectTime int64 ActiveConnNum int @@ -103,12 +104,13 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { return wshrpc.ConnStatus{ Status: conn.Status, Connected: conn.Status == Status_Connected, - WshEnabled: conn.WshEnabled.Load(), Connection: conn.Opts.String(), HasConnected: (conn.LastConnectTime > 0), ActiveConnNum: conn.ActiveConnNum, Error: conn.Error, + WshEnabled: conn.WshEnabled.Load(), WshError: conn.WshError, + NoWshReason: conn.NoWshReason, } } @@ -310,10 +312,18 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { panichandler.PanicHandler("conncontroller:sshSession.Wait", recover()) }() // wait for termination, clear the controller + var waitErr error defer conn.WithLock(func() { + if conn.ConnController != nil { + conn.WshEnabled.Store(false) + conn.NoWshReason = "connserver terminated" + if waitErr != nil { + conn.WshError = fmt.Sprintf("connserver terminated unexpectedly with error: %v", waitErr) + } + } conn.ConnController = nil }) - waitErr := sshSession.Wait() + waitErr = sshSession.Wait() log.Printf("conn controller (%q) terminated: %v", conn.GetName(), waitErr) }() go func() { @@ -642,56 +652,60 @@ func (conn *SSHConn) getConnWshSettings() (bool, bool) { return enableWsh, askBeforeInstall } -func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, error) { +// returns (wsh-enabled, text-reason, wshError) +func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, string, error) { conn.Infof(ctx, "running tryEnableWsh...\n") enableWsh, askBeforeInstall := conn.getConnWshSettings() conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall) if !enableWsh { - return false, nil + return false, "conn:wshenabled set to false", nil } if askBeforeInstall { allowInstall, err := conn.getPermissionToInstallWsh(ctx, clientDisplayName) if err != nil { log.Printf("error getting permission to install wsh: %v\n", err) - return false, err + return false, "error getting user permission to install", err } if !allowInstall { - return false, nil + return false, "user selected not to install wsh extensions", nil } } - // TODO make sure installed err := conn.OpenDomainSocketListener(ctx) if err != nil { conn.Infof(ctx, "ERROR opening domain socket listener: %v\n", err) - return false, fmt.Errorf("error opening domain socket listener: %w", err) + return false, "could not open domain socket", fmt.Errorf("error opening domain socket listener: %w", err) } needsInstall, err := conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server: %v\n", err) - return false, fmt.Errorf("error starting conn server: %w", err) + return false, "error starting connserver", fmt.Errorf("error starting conn server: %w", err) } if needsInstall { conn.Infof(ctx, "connserver needs to be (re)installed\n") err = conn.installWsh(ctx) if err != nil { conn.Infof(ctx, "ERROR installing wsh: %v\n", err) - return false, fmt.Errorf("error installing wsh: %w", err) + return false, "error installing connserver", fmt.Errorf("error installing wsh: %w", err) } needsInstall, err = conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server (after install): %v\n", err) - return false, fmt.Errorf("error starting conn server (after install): %w", err) + return false, "error starting connserver", fmt.Errorf("error starting conn server (after install): %w", err) } if needsInstall { conn.Infof(ctx, "conn server not installed correctly (after install)\n") - return false, fmt.Errorf("conn server not installed correctly (after install)") + return false, "connserver not installed properly", fmt.Errorf("conn server not installed correctly (after install)") } } - return true, nil + return true, "", nil } -func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool) { +func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool, noWshReason string, wshEnableErr error) { conn.WshEnabled.Store(wshEnabled) + conn.SetWshError(wshEnableErr) + conn.WithLock(func() { + conn.NoWshReason = noWshReason + }) config := wconfig.GetWatcher().GetFullConfig() connSettings, ok := config.Connections[conn.GetName()] if ok && connSettings.ConnWshEnabled != nil { @@ -707,10 +721,12 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool) { // doesn't return an error since none of this is required for connection to work } +// returns (connect-error) func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.ConnKeywords) error { conn.Infof(ctx, "connectInternal %s\n", conn.GetName()) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { + conn.Infof(ctx, "ERROR ConnectToClient: %s\n", remote.SimpleMessageFromPossibleConnectionError(err)) log.Printf("error: failed to connect to client %s: %s\n", conn.GetName(), err) return err } @@ -721,12 +737,16 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) - wshInstalled, enableErr := conn.tryEnableWsh(ctx, clientDisplayName) - if enableErr != nil { - conn.Infof(ctx, "ERROR enabling wsh: %v\n", enableErr) - conn.Infof(ctx, "will connect with wsh disabled\n") + wshInstalled, noWshReason, enableErr := conn.tryEnableWsh(ctx, clientDisplayName) + if !wshInstalled { + if enableErr != nil { + conn.Infof(ctx, "ERROR enabling wsh: %v\n", enableErr) + conn.Infof(ctx, "will connect with wsh disabled\n") + } else { + conn.Infof(ctx, "wsh not enabled: %s\n", noWshReason) + } } - conn.persistWshInstalled(ctx, wshInstalled) + conn.persistWshInstalled(ctx, wshInstalled, noWshReason, enableErr) return nil } @@ -752,6 +772,22 @@ func (conn *SSHConn) waitForDisconnect() { }) } +func (conn *SSHConn) SetWshError(err error) { + conn.WithLock(func() { + if err == nil { + conn.WshError = "" + } else { + conn.WshError = err.Error() + } + }) +} + +func (conn *SSHConn) ClearWshError() { + conn.WithLock(func() { + conn.WshError = "" + }) +} + func getConnInternal(opts *remote.SSHOpts) *SSHConn { globalLock.Lock() defer globalLock.Unlock() diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index b0b9272bad..f135b19327 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -23,6 +23,7 @@ import ( "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" + "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/trimquotes" "github.com/wavetermdev/waveterm/pkg/userinput" @@ -72,9 +73,19 @@ type ConnectionError struct { func (ce ConnectionError) Error() string { if ce.CurrentClient == nil { - return fmt.Sprintf("Connecting to %+#v, Error: %v", ce.NextOpts, ce.Err) + return fmt.Sprintf("Connecting to %s, Error: %v", ce.NextOpts, ce.Err) } - return fmt.Sprintf("Connecting from %v to %+#v (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err) + return fmt.Sprintf("Connecting from %v to %s (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err) +} + +func SimpleMessageFromPossibleConnectionError(err error) string { + if err == nil { + return "" + } + if ce, ok := err.(ConnectionError); ok { + return ce.Err.Error() + } + return err.Error() } // This exists to trick the ssh library into continuing to try @@ -142,6 +153,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe return nil, ConnectionError{ConnectionDebugInfo: debugInfo, Err: fmt.Errorf("no identity files remaining")} } identityFile := (*identityFilesPtr)[0] + blocklogger.Infof(connCtx, "[conndebug] trying keyfile %q...\n", identityFile) *identityFilesPtr = (*identityFilesPtr)[1:] privateKey, ok := existingKeys[identityFile] if !ok { @@ -208,6 +220,7 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wshrpc.ConnKe func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string, debugInfo *ConnectionDebugInfo) func() (secret string, err error) { return func() (secret string, err error) { + blocklogger.Infof(connCtx, "[conndebug] Password Authentication requested from connection %s...\n", remoteDisplayName) ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second) defer cancelFn() queryText := fmt.Sprintf( @@ -222,8 +235,10 @@ func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisp } response, err := userinput.GetUserInput(ctx, request) if err != nil { + blocklogger.Infof(connCtx, "[conndebug] ERROR Password Authentication failed: %v\n", SimpleMessageFromPossibleConnectionError(err)) return "", ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } + blocklogger.Infof(connCtx, "[conndebug] got password from user, sending to ssh\n") return response.Text, nil } } @@ -557,7 +572,10 @@ func createClientConfig(connCtx context.Context, sshKeywords *wshrpc.ConnKeyword chosenUser := utilfn.SafeDeref(sshKeywords.SshUser) chosenHostName := utilfn.SafeDeref(sshKeywords.SshHostName) chosenPort := utilfn.SafeDeref(sshKeywords.SshPort) - remoteName := chosenUser + xknownhosts.Normalize(chosenHostName+":"+chosenPort) + remoteName := xknownhosts.Normalize(chosenHostName + ":" + chosenPort) + if chosenUser != "" { + remoteName = chosenUser + "@" + remoteName + } var authSockSigners []ssh.Signer var agentClient agent.ExtendedAgent @@ -619,24 +637,31 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. var err error if currentClient == nil { d := net.Dialer{Timeout: clientConfig.Timeout} + blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", networkAddr) clientConn, err = d.DialContext(ctx, "tcp", networkAddr) if err != nil { + blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) return nil, err } } else { + blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", networkAddr) clientConn, err = currentClient.DialContext(ctx, "tcp", networkAddr) if err != nil { + blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) return nil, err } } c, chans, reqs, err := ssh.NewClientConn(clientConn, networkAddr, clientConfig) if err != nil { + blocklogger.Infof(ctx, "[conndebug] ERROR ssh auth/negotiation: %s\n", SimpleMessageFromPossibleConnectionError(err)) return nil, err } + blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", networkAddr) return ssh.NewClient(c, chans, reqs), nil } func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wshrpc.ConnKeywords) (*ssh.Client, int32, error) { + blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", opts.String(), jumpNum) debugInfo := &ConnectionDebugInfo{ CurrentClient: currentClient, NextOpts: opts, diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 4b55f6369a..2972ca6688 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -494,8 +494,9 @@ type ConnKeywords struct { } type ConnRequest struct { - Host string `json:"host"` - Keywords ConnKeywords `json:"keywords,omitempty"` + Host string `json:"host"` + Keywords ConnKeywords `json:"keywords,omitempty"` + LogBlockId string `json:"logblockid,omitempty"` } const ( @@ -540,6 +541,7 @@ type ConnStatus struct { ActiveConnNum int `json:"activeconnnum"` Error string `json:"error,omitempty"` WshError string `json:"wsherror,omitempty"` + NoWshReason string `json:"nowshreason,omitempty"` } type WebSelectorOpts struct { diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 5b133300d4..9ee8aac623 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -610,16 +610,23 @@ func (ws *WshServer) WslStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, return rtn, nil } -func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsureData) error { - if data.LogBlockId != "" { - block, err := wstore.DBMustGet[*waveobj.Block](ctx, data.LogBlockId) - if err == nil { - connDebug := block.Meta.GetString(waveobj.MetaKey_TermDebugConn, "") - if connDebug != "" { - ctx = blocklogger.ContextWithLogBlockId(ctx, data.LogBlockId, connDebug == "debug") - } - } +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_TermDebugConn, "") + if connDebug == "" { + return ctx + } + return blocklogger.ContextWithLogBlockId(ctx, logBlockId, connDebug == "debug") +} + +func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsureData) error { + ctx = termCtxWithLogBlockId(ctx, data.LogBlockId) if strings.HasPrefix(data.ConnName, "wsl://") { distroName := strings.TrimPrefix(data.ConnName, "wsl://") return wsl.EnsureConnection(ctx, distroName) @@ -648,6 +655,7 @@ func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) } func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error { + ctx = termCtxWithLogBlockId(ctx, connRequest.LogBlockId) connName := connRequest.Host if strings.HasPrefix(connName, "wsl://") { distroName := strings.TrimPrefix(connName, "wsl://") @@ -731,9 +739,7 @@ func (ws *WshServer) DismissWshFailCommand(ctx context.Context, connName string) if conn == nil { return fmt.Errorf("connection %s not found", connName) } - conn.WithLock(func() { - conn.WshError = "" - }) + conn.ClearWshError() conn.FireConnChangeEvent() return nil } From b547e8342a32d12721cd9040daebe600442cb578 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 9 Jan 2025 21:44:58 -0800 Subject: [PATCH 12/16] add stopping blockcontroller log info --- pkg/blockcontroller/blockcontroller.go | 11 +++++++++++ pkg/wshrpc/wshserver/wshserver.go | 1 + 2 files changed, 12 insertions(+) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index b87948a485..df1ff1be7a 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -16,6 +16,7 @@ import ( "sync/atomic" "time" + "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote" @@ -757,6 +758,13 @@ func getOrCreateBlockController(tabId string, blockId string, controllerName str return bc } +func formatConnNameForLog(connName string) string { + if connName == "" { + return "local" + } + return connName +} + func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts *waveobj.RuntimeOpts, force bool) error { if tabId == "" || blockId == "" { return fmt.Errorf("invalid tabId or blockId passed to ResyncController") @@ -767,6 +775,7 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts } if force { StopBlockController(blockId) + time.Sleep(100 * time.Millisecond) // TODO see if we can remove this (the "process finished with exit code" message comes out after we start reconnecting otherwise) } connName := blockData.Meta.GetString(waveobj.MetaKey_Connection, "") controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "") @@ -782,8 +791,10 @@ func ResyncController(ctx context.Context, tabId string, blockId string, rtOpts if curBc != nil { bcStatus := curBc.GetRuntimeStatus() if bcStatus.ShellProcStatus == Status_Running && bcStatus.ShellProcConnName != connName { + blocklogger.Infof(ctx, "\n[conndebug] stopping blockcontroller due to conn change %q => %q\n", formatConnNameForLog(bcStatus.ShellProcConnName), formatConnNameForLog(connName)) log.Printf("stopping blockcontroller %s due to conn change\n", blockId) StopBlockControllerAndSetStatus(blockId, Status_Init) + time.Sleep(100 * time.Millisecond) // TODO see if we can remove this (the "process finished with exit code" message comes out after we start reconnecting otherwise) } } // now if there is a conn, ensure it is connected diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 9ee8aac623..b1edeca7ca 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -238,6 +238,7 @@ func (ws *WshServer) ControllerStopCommand(ctx context.Context, blockId string) } func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error { + ctx = termCtxWithLogBlockId(ctx, data.BlockId) return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts, data.ForceRestart) } From 14c80449f261f1be53efdd535026a9eb93129f17 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 10:38:06 -0800 Subject: [PATCH 13/16] remove CheckAndInstallWsh, now replaced by InstallWsh. . reinstall now takes a logblockid. --- cmd/wsh/cmd/wshcmd-conn.go | 8 ++- frontend/app/store/wshclientapi.ts | 4 +- frontend/types/gotypes.d.ts | 4 +- pkg/remote/conncontroller/conncontroller.go | 78 ++------------------- pkg/wshrpc/wshclient/wshclient.go | 4 +- pkg/wshrpc/wshrpctypes.go | 6 +- pkg/wshrpc/wshserver/wshserver.go | 8 ++- 7 files changed, 27 insertions(+), 85 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-conn.go b/cmd/wsh/cmd/wshcmd-conn.go index 344660bc60..2b2d3a98fc 100644 --- a/cmd/wsh/cmd/wshcmd-conn.go +++ b/cmd/wsh/cmd/wshcmd-conn.go @@ -128,7 +128,11 @@ func connReinstallRun(cmd *cobra.Command, args []string) error { if err := validateConnectionName(connName); err != nil { return err } - err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000}) + data := wshrpc.ConnExtData{ + ConnName: connName, + LogBlockId: RpcContext.BlockId, + } + err := wshclient.ConnReinstallWshCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000}) if err != nil { return fmt.Errorf("reinstalling connection: %w", err) } @@ -190,7 +194,7 @@ func connEnsureRun(cmd *cobra.Command, args []string) error { if err := validateConnectionName(connName); err != nil { return err } - data := wshrpc.ConnEnsureData{ + data := wshrpc.ConnExtData{ ConnName: connName, LogBlockId: RpcContext.BlockId, } diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 80adb40fbd..2d97a1424f 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -38,7 +38,7 @@ class RpcApiType { } // command "connensure" [call] - ConnEnsureCommand(client: WshClient, data: ConnEnsureData, opts?: RpcOpts): Promise { + ConnEnsureCommand(client: WshClient, data: ConnExtData, opts?: RpcOpts): Promise { return client.wshRpcCall("connensure", data, opts); } @@ -48,7 +48,7 @@ class RpcApiType { } // command "connreinstallwsh" [call] - ConnReinstallWshCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + ConnReinstallWshCommand(client: WshClient, data: ConnExtData, opts?: RpcOpts): Promise { return client.wshRpcCall("connreinstallwsh", data, opts); } diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index f2cb60193a..e0235fcaeb 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -292,8 +292,8 @@ declare global { metamaptype: MetaType; }; - // wshrpc.ConnEnsureData - type ConnEnsureData = { + // wshrpc.ConnExtData + type ConnExtData = { connname: string; logblockid?: string; }; diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index d8b719332e..617a2aae2f 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -418,84 +418,20 @@ func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDispla return true, nil } -func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName string, opts *WshInstallOpts) error { - if opts == nil { - opts = &WshInstallOpts{} - } +func (conn *SSHConn) InstallWsh(ctx context.Context) error { + conn.Infof(ctx, "running installWsh...\n") client := conn.GetClient() if client == nil { - return fmt.Errorf("client is nil") - } - // check that correct wsh extensions are installed - expectedVersion := fmt.Sprintf("v%s", wavebase.WaveVersion) - clientVersion, err := remote.GetWshVersion(client) - if err == nil && !opts.Force && semver.Compare(clientVersion, expectedVersion) >= 0 { - return nil + conn.Infof(ctx, "ERROR ssh client is not connected, cannot install\n") + return fmt.Errorf("ssh client is not connected, cannot install") } - var queryText string - var title string - if opts.Force { - queryText = fmt.Sprintf("ReInstalling Wave Shell Extensions (%s) on `%s`\n", wavebase.WaveVersion, clientDisplayName) - title = "Install Wave Shell Extensions" - } else if err != nil { - queryText = fmt.Sprintf("Wave requires Wave Shell Extensions to be \n"+ - "installed on `%s` \n"+ - "to ensure a seamless experience. \n\n"+ - "Would you like to install them?", clientDisplayName) - title = "Install Wave Shell Extensions" - } else { - // don't ask for upgrading the version - opts.NoUserPrompt = true - } - if !opts.NoUserPrompt { - request := &userinput.UserInputRequest{ - ResponseType: "confirm", - QueryText: queryText, - Title: title, - Markdown: true, - CheckBoxMsg: "Automatically install for all connections", - OkLabel: "Install wsh", - CancelLabel: "No wsh", - } - response, err := userinput.GetUserInput(ctx, request) - if err != nil { - return err - } - if !response.Confirm { - meta := make(map[string]any) - meta["conn:wshenabled"] = false - err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta) - if err != nil { - log.Printf("warning: error writing to connections file: %v", err) - } - return &WshInstallSkipError{} - } - if response.CheckboxStat { - meta := waveobj.MetaMapType{ - wconfig.ConfigKey_ConnAskBeforeWshInstall: false, - } - err := wconfig.SetBaseConfigValue(meta) - if err != nil { - return fmt.Errorf("error setting conn:askbeforewshinstall value: %w", err) - } - } - } - err = conn.installWsh(ctx) - if err != nil { - return err - } - return nil -} - -func (conn *SSHConn) installWsh(ctx context.Context) error { - conn.Infof(ctx, "running installWsh...\n") - clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(conn.GetClient())) + clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(client)) if err != nil { conn.Infof(ctx, "ERROR detecting client platform: %v\n", err) return err } conn.Infof(ctx, "detected remote platform os:%s arch:%s\n", clientOs, clientArch) - err = remote.CpWshToRemote(ctx, conn.GetClient(), clientOs, clientArch) + err = remote.CpWshToRemote(ctx, client, clientOs, clientArch) if err != nil { conn.Infof(ctx, "ERROR copying wsh binary to remote: %v\n", err) return fmt.Errorf("error copying wsh binary to remote: %w", err) @@ -682,7 +618,7 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } if needsInstall { conn.Infof(ctx, "connserver needs to be (re)installed\n") - err = conn.installWsh(ctx) + err = conn.InstallWsh(ctx) if err != nil { conn.Infof(ctx, "ERROR installing wsh: %v\n", err) return false, "error installing connserver", fmt.Errorf("error installing wsh: %w", err) diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index d1a1d10444..da524bf5ae 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -50,7 +50,7 @@ func ConnDisconnectCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) } // command "connensure", wshserver.ConnEnsureCommand -func ConnEnsureCommand(w *wshutil.WshRpc, data wshrpc.ConnEnsureData, opts *wshrpc.RpcOpts) error { +func ConnEnsureCommand(w *wshutil.WshRpc, data wshrpc.ConnExtData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connensure", data, opts) return err } @@ -62,7 +62,7 @@ func ConnListCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) ([]string, error) } // command "connreinstallwsh", wshserver.ConnReinstallWshCommand -func ConnReinstallWshCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error { +func ConnReinstallWshCommand(w *wshutil.WshRpc, data wshrpc.ConnExtData, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connreinstallwsh", data, opts) return err } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 2972ca6688..c88267c352 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -155,8 +155,8 @@ type WshRpcInterface interface { // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) WslStatusCommand(ctx context.Context) ([]ConnStatus, error) - ConnEnsureCommand(ctx context.Context, data ConnEnsureData) error - ConnReinstallWshCommand(ctx context.Context, connName string) error + ConnEnsureCommand(ctx context.Context, data ConnExtData) error + ConnReinstallWshCommand(ctx context.Context, data ConnExtData) error ConnConnectCommand(ctx context.Context, connRequest ConnRequest) error ConnDisconnectCommand(ctx context.Context, connName string) error ConnListCommand(ctx context.Context) ([]string, error) @@ -655,7 +655,7 @@ type ActivityUpdate struct { Conn map[string]int `json:"conn,omitempty"` } -type ConnEnsureData struct { +type ConnExtData struct { ConnName string `json:"connname"` LogBlockId string `json:"logblockid,omitempty"` } diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index b1edeca7ca..047f0445dc 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -626,7 +626,7 @@ func termCtxWithLogBlockId(ctx context.Context, logBlockId string) context.Conte return blocklogger.ContextWithLogBlockId(ctx, logBlockId, connDebug == "debug") } -func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnEnsureData) error { +func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnExtData) error { ctx = termCtxWithLogBlockId(ctx, data.LogBlockId) if strings.HasPrefix(data.ConnName, "wsl://") { distroName := strings.TrimPrefix(data.ConnName, "wsl://") @@ -677,7 +677,9 @@ func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc. return conn.Connect(ctx, &connRequest.Keywords) } -func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName string) error { +func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, data wshrpc.ConnExtData) error { + ctx = termCtxWithLogBlockId(ctx, data.LogBlockId) + connName := data.ConnName if strings.HasPrefix(connName, "wsl://") { distroName := strings.TrimPrefix(connName, "wsl://") conn := wsl.GetWslConn(ctx, distroName, false) @@ -694,7 +696,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, connName strin if conn == nil { return fmt.Errorf("connection not found: %s", connName) } - return conn.CheckAndInstallWsh(ctx, connName, &conncontroller.WshInstallOpts{Force: true, NoUserPrompt: true}) + return conn.InstallWsh(ctx) } func (ws *WshServer) ConnListCommand(ctx context.Context) ([]string, error) { From 74c402c8255fb9c228ef2c262335912ae793a050 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 10:51:49 -0800 Subject: [PATCH 14/16] more complicated rtn from tryEnableWsh --- pkg/remote/conncontroller/conncontroller.go | 106 ++++++++++++-------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 617a2aae2f..a34d052bf8 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -63,6 +63,7 @@ type SSHConn struct { Error string WshError string NoWshReason string + WshVersion string HasWaiter *atomic.Bool LastConnectTime int64 ActiveConnNum int @@ -224,31 +225,33 @@ func (conn *SSHConn) OpenDomainSocketListener(ctx context.Context) error { } // expects the output of `wsh version` which looks like `wsh v0.10.4` or "not-installed" -func isWshVersionUpToDate(wshVersionLine string) (bool, error) { +// returns (up-to-date, semver, error) +// if not up to date, or error, version might be "" +func isWshVersionUpToDate(wshVersionLine string) (bool, string, error) { wshVersionLine = strings.TrimSpace(wshVersionLine) if wshVersionLine == "not-installed" { - return false, nil + return false, "", nil } parts := strings.Fields(wshVersionLine) if len(parts) != 2 { - return false, fmt.Errorf("unexpected version format: %s", wshVersionLine) + return false, "", fmt.Errorf("unexpected version format: %s", wshVersionLine) } clientVersion := parts[1] expectedVersion := fmt.Sprintf("v%s", wavebase.WaveVersion) if semver.Compare(clientVersion, expectedVersion) < 0 { - return false, nil + return false, clientVersion, nil } - return true, nil + return true, clientVersion, nil } -// returns (needsInstall, error) -func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { +// returns (needsInstall, clientVersion, error) +func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, error) { conn.Infof(ctx, "running StartConnServer...\n") allowed := WithLockRtn(conn, func() bool { return conn.Status == Status_Connecting }) if !allowed { - return false, fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus()) + return false, "", fmt.Errorf("cannot start conn server for %q when status is %q", conn.GetName(), conn.GetStatus()) } client := conn.GetClient() wshPath := remote.GetWshPath(client) @@ -259,49 +262,49 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { sockName := conn.GetDomainSocketName() jwtToken, err := wshutil.MakeClientJWTToken(rpcCtx, sockName) if err != nil { - return false, fmt.Errorf("unable to create jwt token for conn controller: %w", err) + return false, "", fmt.Errorf("unable to create jwt token for conn controller: %w", err) } sshSession, err := client.NewSession() if err != nil { - return false, fmt.Errorf("unable to create ssh session for conn controller: %w", err) + return false, "", fmt.Errorf("unable to create ssh session for conn controller: %w", err) } pipeRead, pipeWrite := io.Pipe() sshSession.Stdout = pipeWrite sshSession.Stderr = pipeWrite stdinPipe, err := sshSession.StdinPipe() if err != nil { - return false, fmt.Errorf("unable to get stdin pipe: %w", err) + return false, "", fmt.Errorf("unable to get stdin pipe: %w", err) } cmdStr := fmt.Sprintf(ConnServerCmdTemplate, wshPath, wshPath) log.Printf("starting conn controller: %s\n", cmdStr) shWrappedCmdStr := fmt.Sprintf("sh -c %s", genconn.HardQuote(cmdStr)) err = sshSession.Start(shWrappedCmdStr) if err != nil { - return false, fmt.Errorf("unable to start conn controller command: %w", err) + return false, "", fmt.Errorf("unable to start conn controller command: %w", err) } linesChan := wshutil.StreamToLinesChan(pipeRead) versionLine, err := wshutil.ReadLineWithTimeout(linesChan, 2*time.Second) if err != nil { sshSession.Close() - return false, fmt.Errorf("error reading wsh version: %w", err) + return false, "", fmt.Errorf("error reading wsh version: %w", err) } conn.Infof(ctx, "got connserver version: %s\n", strings.TrimSpace(versionLine)) - isUpToDate, err := isWshVersionUpToDate(versionLine) + isUpToDate, clientVersion, err := isWshVersionUpToDate(versionLine) if err != nil { sshSession.Close() - return false, fmt.Errorf("error checking wsh version: %w", err) + return false, "", fmt.Errorf("error checking wsh version: %w", err) } conn.Infof(ctx, "connserver update to date: %v\n", isUpToDate) if !isUpToDate { sshSession.Close() - return true, nil + return true, clientVersion, nil } // write the jwt conn.Infof(ctx, "writing jwt token to connserver\n") _, err = fmt.Fprintf(stdinPipe, "%s\n", jwtToken) if err != nil { sshSession.Close() - return false, fmt.Errorf("failed to write JWT token: %w", err) + return false, clientVersion, fmt.Errorf("failed to write JWT token: %w", err) } conn.WithLock(func() { conn.ConnController = sshSession @@ -347,11 +350,11 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, error) { defer cancelFn() err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn)) if err != nil { - return false, fmt.Errorf("timeout waiting for connserver to register") + return false, clientVersion, fmt.Errorf("timeout waiting for connserver to register") } time.Sleep(300 * time.Millisecond) // TODO remove this sleep (but we need to wait until connserver is "ready") conn.Infof(ctx, "connserver is registered and ready\n") - return false, nil + return false, clientVersion, nil } type WshInstallOpts struct { @@ -588,59 +591,74 @@ func (conn *SSHConn) getConnWshSettings() (bool, bool) { return enableWsh, askBeforeInstall } -// returns (wsh-enabled, text-reason, wshError) -func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) (bool, string, error) { +type WshCheckResult struct { + WshEnabled bool + ClientVersion string + NoWshReason string + WshError error +} + +// returns (wsh-enabled, clientVersion, text-reason, wshError) +func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) WshCheckResult { conn.Infof(ctx, "running tryEnableWsh...\n") enableWsh, askBeforeInstall := conn.getConnWshSettings() conn.Infof(ctx, "wsh settings enable:%v ask:%v\n", enableWsh, askBeforeInstall) if !enableWsh { - return false, "conn:wshenabled set to false", nil + return WshCheckResult{NoWshReason: "conn:wshenabled set to false"} } if askBeforeInstall { allowInstall, err := conn.getPermissionToInstallWsh(ctx, clientDisplayName) if err != nil { log.Printf("error getting permission to install wsh: %v\n", err) - return false, "error getting user permission to install", err + return WshCheckResult{NoWshReason: "error getting user permission to install", WshError: err} } if !allowInstall { - return false, "user selected not to install wsh extensions", nil + return WshCheckResult{NoWshReason: "user selected not to install wsh extensions"} } } err := conn.OpenDomainSocketListener(ctx) if err != nil { conn.Infof(ctx, "ERROR opening domain socket listener: %v\n", err) - return false, "could not open domain socket", fmt.Errorf("error opening domain socket listener: %w", err) + err = fmt.Errorf("error opening domain socket listener: %w", err) + return WshCheckResult{NoWshReason: "error opening domain socket", WshError: err} } - needsInstall, err := conn.StartConnServer(ctx) + needsInstall, clientVersion, err := conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server: %v\n", err) - return false, "error starting connserver", fmt.Errorf("error starting conn server: %w", err) + err = fmt.Errorf("error starting conn server: %w", err) + return WshCheckResult{NoWshReason: "error starting connserver", WshError: err} } if needsInstall { conn.Infof(ctx, "connserver needs to be (re)installed\n") err = conn.InstallWsh(ctx) if err != nil { conn.Infof(ctx, "ERROR installing wsh: %v\n", err) - return false, "error installing connserver", fmt.Errorf("error installing wsh: %w", err) + err = fmt.Errorf("error installing wsh: %w", err) + return WshCheckResult{NoWshReason: "error installing wsh/connserver", WshError: err} } - needsInstall, err = conn.StartConnServer(ctx) + needsInstall, clientVersion, err = conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server (after install): %v\n", err) - return false, "error starting connserver", fmt.Errorf("error starting conn server (after install): %w", err) + err = fmt.Errorf("error starting conn server (after install): %w", err) + return WshCheckResult{NoWshReason: "error starting connserver", WshError: err} } if needsInstall { conn.Infof(ctx, "conn server not installed correctly (after install)\n") - return false, "connserver not installed properly", fmt.Errorf("conn server not installed correctly (after install)") + err = fmt.Errorf("conn server not installed correctly (after install)") + return WshCheckResult{NoWshReason: "connserver not installed properly", WshError: err} } + return WshCheckResult{WshEnabled: true, ClientVersion: clientVersion} + } else { + return WshCheckResult{WshEnabled: true, ClientVersion: clientVersion} } - return true, "", nil } -func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool, noWshReason string, wshEnableErr error) { - conn.WshEnabled.Store(wshEnabled) - conn.SetWshError(wshEnableErr) +func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckResult) { + conn.WshEnabled.Store(result.WshEnabled) + conn.SetWshError(result.WshError) conn.WithLock(func() { - conn.NoWshReason = noWshReason + conn.NoWshReason = result.NoWshReason + conn.WshVersion = result.ClientVersion }) config := wconfig.GetWatcher().GetFullConfig() connSettings, ok := config.Connections[conn.GetName()] @@ -648,10 +666,10 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, wshEnabled bool, n return } meta := make(map[string]any) - meta["conn:wshenabled"] = wshEnabled + meta["conn:wshenabled"] = result.WshEnabled err := wconfig.SetConnectionsConfigValue(conn.GetName(), meta) if err != nil { - conn.Infof(ctx, "WARN could not write conn:wshenabled=%v to connections.json: %v\n", wshEnabled, err) + conn.Infof(ctx, "WARN could not write conn:wshenabled=%v to connections.json: %v\n", result.WshEnabled, err) log.Printf("warning: error writing to connections file: %v", err) } // doesn't return an error since none of this is required for connection to work @@ -673,16 +691,16 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) - wshInstalled, noWshReason, enableErr := conn.tryEnableWsh(ctx, clientDisplayName) - if !wshInstalled { - if enableErr != nil { - conn.Infof(ctx, "ERROR enabling wsh: %v\n", enableErr) + wshResult := conn.tryEnableWsh(ctx, clientDisplayName) + if !wshResult.WshEnabled { + if wshResult.WshError != nil { + conn.Infof(ctx, "ERROR enabling wsh: %v\n", wshResult.WshError) conn.Infof(ctx, "will connect with wsh disabled\n") } else { - conn.Infof(ctx, "wsh not enabled: %s\n", noWshReason) + conn.Infof(ctx, "wsh not enabled: %s\n", wshResult.NoWshReason) } } - conn.persistWshInstalled(ctx, wshInstalled, noWshReason, enableErr) + conn.persistWshInstalled(ctx, wshResult) return nil } From 446a93c910d180c4b38a5e5e7e6f8171ccc4daef Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 13:50:08 -0800 Subject: [PATCH 15/16] swith from term:debugconn to term:conndebug for consistency --- frontend/app/view/term/term.tsx | 8 ++++---- frontend/types/gotypes.d.ts | 3 ++- pkg/remote/connutil.go | 27 --------------------------- pkg/waveobj/metaconsts.go | 2 +- pkg/waveobj/wtypemeta.go | 2 +- pkg/wshrpc/wshrpctypes.go | 7 ++++--- pkg/wshrpc/wshserver/wshserver.go | 2 +- 7 files changed, 13 insertions(+), 38 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index b357606965..b2c521a051 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -682,7 +682,7 @@ class TermViewModel implements ViewModel { }, }); } - const debugConn = blockData?.meta?.["term:debugconn"]; + const debugConn = blockData?.meta?.["term:conndebug"]; fullMenu.push({ label: "Debug Connection", submenu: [ @@ -693,7 +693,7 @@ class TermViewModel implements ViewModel { click: () => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), - meta: { "term:debugconn": null }, + meta: { "term:conndebug": null }, }); }, }, @@ -704,7 +704,7 @@ class TermViewModel implements ViewModel { click: () => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), - meta: { "term:debugconn": "info" }, + meta: { "term:conndebug": "info" }, }); }, }, @@ -715,7 +715,7 @@ class TermViewModel implements ViewModel { click: () => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), - meta: { "term:debugconn": "debug" }, + meta: { "term:conndebug": "debug" }, }); }, }, diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index e0235fcaeb..7b14795a33 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -303,6 +303,7 @@ declare global { "conn:wshenabled"?: boolean; "conn:askbeforewshinstall"?: boolean; "conn:overrideconfig"?: boolean; + "conn:wshpath"?: string; "display:hidden"?: boolean; "display:order"?: number; "term:*"?: boolean; @@ -508,7 +509,7 @@ declare global { "term:vdomtoolbarblockid"?: string; "term:transparency"?: number; "term:allowbracketedpaste"?: boolean; - "term:debugconn"?: string; + "term:conndebug"?: string; "web:zoom"?: number; "web:hidenav"?: boolean; "markdown:fontsize"?: number; diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index 7c8a1dff1a..0795936069 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -20,7 +20,6 @@ import ( "github.com/wavetermdev/waveterm/pkg/util/shellutil" "github.com/wavetermdev/waveterm/pkg/wavebase" "golang.org/x/crypto/ssh" - "golang.org/x/mod/semver" ) var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`) @@ -55,32 +54,6 @@ func DetectShell(client *ssh.Client) (string, error) { return fmt.Sprintf(`"%s"`, strings.TrimSpace(string(out))), nil } -// returns a valid semver version string -func GetWshVersion(client *ssh.Client) (string, error) { - wshPath := GetWshPath(client) - - session, err := client.NewSession() - if err != nil { - return "", err - } - - out, err := session.Output(wshPath + " version") - if err != nil { - return "", err - } - // output is expected to be in the form of "wsh v0.10.4" - // should strip off the "wsh" prefix, and return a semver object - fields := strings.Fields(strings.TrimSpace(string(out))) - if len(fields) != 2 { - return "", fmt.Errorf("unexpected output from wsh version: %s", out) - } - wshVersion := strings.TrimSpace(fields[1]) - if !semver.IsValid(wshVersion) { - return "", fmt.Errorf("invalid semver version: %s", wshVersion) - } - return wshVersion, nil -} - func GetWshPath(client *ssh.Client) string { defaultPath := wavebase.RemoteFullWshBinPath session, err := client.NewSession() diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 292322abed..d576e6bf19 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -95,7 +95,7 @@ const ( MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid" MetaKey_TermTransparency = "term:transparency" MetaKey_TermAllowBracketedPaste = "term:allowbracketedpaste" - MetaKey_TermDebugConn = "term:debugconn" + MetaKey_TermConnDebug = "term:conndebug" MetaKey_WebZoom = "web:zoom" MetaKey_WebHideNav = "web:hidenav" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index a4292abfbf..01a1327925 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -96,7 +96,7 @@ type MetaTSType struct { TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5 TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"` - TermDebugConn string `json:"term:debugconn,omitempty"` // null, info, debug + TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug WebZoom float64 `json:"web:zoom,omitempty"` WebHideNav *bool `json:"web:hidenav,omitempty"` diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index c88267c352..7d49e837ad 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -465,9 +465,10 @@ type CommandRemoteWriteFileData struct { } type ConnKeywords struct { - ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"` - ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` - ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"` + ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"` + ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` + ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"` + ConnWshPath string `json:"conn:wshpath,omitempty"` DisplayHidden *bool `json:"display:hidden,omitempty"` DisplayOrder float32 `json:"display:order,omitempty"` diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 047f0445dc..3e0bc25547 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -619,7 +619,7 @@ func termCtxWithLogBlockId(ctx context.Context, logBlockId string) context.Conte if err != nil { return ctx } - connDebug := block.Meta.GetString(waveobj.MetaKey_TermDebugConn, "") + connDebug := block.Meta.GetString(waveobj.MetaKey_TermConnDebug, "") if connDebug == "" { return ctx } From e0ba380341fd70e09f78a8354f586951878b238d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 13:56:48 -0800 Subject: [PATCH 16/16] add wshversion to connstatus --- frontend/types/gotypes.d.ts | 1 + pkg/remote/conncontroller/conncontroller.go | 1 + pkg/wshrpc/wshrpctypes.go | 1 + 3 files changed, 3 insertions(+) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 7b14795a33..7b34fa3f81 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -344,6 +344,7 @@ declare global { error?: string; wsherror?: string; nowshreason?: string; + wshversion?: string; }; // wshrpc.CpuDataRequest diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index a34d052bf8..057b19c576 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -112,6 +112,7 @@ func (conn *SSHConn) DeriveConnStatus() wshrpc.ConnStatus { WshEnabled: conn.WshEnabled.Load(), WshError: conn.WshError, NoWshReason: conn.NoWshReason, + WshVersion: conn.WshVersion, } } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 7d49e837ad..d93172be18 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -543,6 +543,7 @@ type ConnStatus struct { Error string `json:"error,omitempty"` WshError string `json:"wsherror,omitempty"` NoWshReason string `json:"nowshreason,omitempty"` + WshVersion string `json:"wshversion,omitempty"` } type WebSelectorOpts struct {