From 011c1cfa44e0d94d027c15620a591b9f0150c5bb Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 16:06:25 -0800 Subject: [PATCH 01/16] unset ZDOTDIR if unmodified, at the end of zlogin --- pkg/util/shellutil/shellutil.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 79f15e0ee1..864874dc4d 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -56,9 +56,15 @@ fi ZshStartup_Zlogin = ` # Source the original zlogin [ -f ~/.zlogin ] && source ~/.zlogin + +# Unset ZDOTDIR only if it hasn't been modified +if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then + unset ZDOTDIR +fi ` ZshStartup_Zshenv = ` +WAVETERM_ZDOTDIR="$ZDOTDIR" [ -f ~/.zshenv ] && source ~/.zshenv ` From 8b2c54a9c94e36dd2e692bf81437951bb8f1bd6e Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 10 Jan 2025 16:18:16 -0800 Subject: [PATCH 02/16] run our path modifications, even if zdotdir is changed by he user's zshenv --- pkg/util/shellutil/shellutil.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 864874dc4d..debdf8b90e 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -44,9 +44,12 @@ const ( ` ZshStartup_Zshrc = ` -# Source the original zshrc -[ -f ~/.zshrc ] && source ~/.zshrc +# Source the original zshrc only if ZDOTDIR has not been changed +if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then + [ -f ~/.zshrc ] && source ~/.zshrc +fi +# Custom additions export PATH={{.WSHBINDIR}}:$PATH if [[ -n ${_comps+x} ]]; then source <(wsh completion zsh) @@ -64,8 +67,18 @@ fi ` ZshStartup_Zshenv = ` +# Store the initial ZDOTDIR value WAVETERM_ZDOTDIR="$ZDOTDIR" + +# Source the original zshenv [ -f ~/.zshenv ] && source ~/.zshenv + +# Detect if ZDOTDIR has changed +if [ "$ZDOTDIR" != "$WAVETERM_ZDOTDIR" ]; then + # If changed, manually source your custom zshrc from the original WAVETERM_ZDOTDIR + [ -f "$WAVETERM_ZDOTDIR/.zshrc" ] && source "$WAVETERM_ZDOTDIR/.zshrc" +fi + ` BashStartup_Bashrc = ` From eee1a3c6d92b0e304be57ca909a725b2bd3e8824 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 11:52:48 -0800 Subject: [PATCH 03/16] working on mods for StartRemoteShellProc --- pkg/shellexec/shellexec.go | 53 ++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 1529332461..42cee2b685 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -33,6 +33,14 @@ import ( const DefaultGracefulKillWait = 400 * time.Millisecond +const ( + ShellType_bash = "bash" + ShellType_zsh = "zsh" + ShellType_fish = "fish" + ShellType_pwsh = "pwsh" + ShellType_unknown = "unknown" +) + type CommandOptsType struct { Interactive bool `json:"interactive,omitempty"` Login bool `json:"login,omitempty"` @@ -148,6 +156,23 @@ func (pp *PipePty) WriteString(s string) (n int, err error) { return pp.Write([]byte(s)) } +func getShellTypeFromShellPath(shellPath string) string { + shellBase := filepath.Base(shellPath) + if strings.Contains(shellBase, "bash") { + return ShellType_bash + } + if strings.Contains(shellBase, "zsh") { + return ShellType_zsh + } + if strings.Contains(shellBase, "fish") { + return ShellType_fish + } + if strings.Contains(shellBase, "pwsh") || strings.Contains(shellBase, "powershell") { + return ShellType_pwsh + } + return ShellType_unknown +} + func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *wsl.WslConn) (*ShellProc, error) { utilCtx, cancelFn := context.WithTimeout(ctx, 2*time.Second) defer cancelFn() @@ -296,7 +321,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm } var shellOpts []string var cmdCombined string - log.Printf("detected shell: %s", shellPath) + log.Printf("detected shell %q for conn %q\n", shellPath, conn.GetName()) err := remote.InstallClientRcFiles(client) if err != nil { @@ -304,23 +329,22 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm return nil, err } shellOpts = append(shellOpts, cmdOpts.ShellOpts...) - - homeDir := remote.GetHomeDir(client) + shellType := getShellTypeFromShellPath(shellPath) if cmdStr == "" { /* transform command in order to inject environment vars */ - if isBashShell(shellPath) { + if shellType == ShellType_bash { log.Printf("recognized as bash shell") // add --rcfile // cant set -l or -i with --rcfile - shellOpts = append(shellOpts, "--rcfile", fmt.Sprintf(`"%s"/.waveterm/%s/.bashrc`, homeDir, shellutil.BashIntegrationDir)) - } else if isFishShell(shellPath) { - carg := fmt.Sprintf(`"set -x PATH \"%s\"/.waveterm/%s $PATH"`, homeDir, shellutil.WaveHomeBinDir) + shellOpts = append(shellOpts, "--rcfile", fmt.Sprintf(`~/.waveterm/%s/.bashrc`, shellutil.BashIntegrationDir)) + } else if shellType == ShellType_fish { + carg := fmt.Sprintf(`"set -x PATH ~/.waveterm/%s $PATH"`, shellutil.WaveHomeBinDir) shellOpts = append(shellOpts, "-C", carg) - } else if remote.IsPowershell(shellPath) { + } else if shellType == ShellType_pwsh { // powershell is weird about quoted path executables and requires an ampersand first shellPath = "& " + shellPath - shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", fmt.Sprintf("%s/.waveterm/%s/wavepwsh.ps1", homeDir, shellutil.PwshIntegrationDir)) + shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", fmt.Sprintf("~/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir)) } else { if cmdOpts.Login { shellOpts = append(shellOpts, "-l") @@ -373,20 +397,15 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm session.Setenv(envKey, envVal) } - if isZshShell(shellPath) { - cmdCombined = fmt.Sprintf(`ZDOTDIR="%s/.waveterm/%s" %s`, homeDir, shellutil.ZshIntegrationDir, cmdCombined) + if shellType == ShellType_zsh { + cmdCombined = fmt.Sprintf(`ZDOTDIR=~/.waveterm/%s %s`, shellutil.ZshIntegrationDir, cmdCombined) } jwtToken, ok := cmdOpts.Env[wshutil.WaveJwtTokenVarName] if !ok { return nil, fmt.Errorf("no jwt token provided to connection") } - - if remote.IsPowershell(shellPath) { - cmdCombined = fmt.Sprintf(`$env:%s="%s"; %s`, wshutil.WaveJwtTokenVarName, jwtToken, cmdCombined) - } else { - cmdCombined = fmt.Sprintf(`%s=%s %s`, wshutil.WaveJwtTokenVarName, jwtToken, cmdCombined) - } + cmdCombined = fmt.Sprintf(`%s=%s %s`, wshutil.WaveJwtTokenVarName, jwtToken, cmdCombined) session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) sessionWrap := MakeSessionWrap(session, cmdCombined, pipePty) From d5e9b21c649da00061d38bc51c82262c3cfdcaa8 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 11:53:05 -0800 Subject: [PATCH 04/16] up file limit for wsh ai --- cmd/wsh/cmd/wshcmd-ai.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-ai.go b/cmd/wsh/cmd/wshcmd-ai.go index 0b754eb90f..71f175089a 100644 --- a/cmd/wsh/cmd/wshcmd-ai.go +++ b/cmd/wsh/cmd/wshcmd-ai.go @@ -142,8 +142,8 @@ func aiRun(cmd *cobra.Command, args []string) (rtnErr error) { if message.Len() == 0 { return fmt.Errorf("message is empty") } - if message.Len() > 10*1024 { - return fmt.Errorf("current max message size is 10k") + if message.Len() > 50*1024 { + return fmt.Errorf("current max message size is 50k") } messageData := wshrpc.AiMessageData{ From dcb26e26d78f7dc05c5c4141e12e92d3bdbb5630 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 11:54:08 -0800 Subject: [PATCH 05/16] remove log message, put bookmarks into a const (temp) --- frontend/app/view/preview/preview.tsx | 34 ++++++++++----------------- frontend/app/view/waveai/waveai.tsx | 1 - 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index ece27f24fc..7add7e3ded 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -38,6 +38,15 @@ import "./preview.scss"; const MaxFileSize = 1024 * 1024 * 10; // 10MB const MaxCSVSize = 1024 * 1024 * 1; // 1MB +// TODO drive this using config +const BOOKMARKS: { label: string; path: string }[] = [ + { label: "Home", path: "~" }, + { label: "Desktop", path: "~/Desktop" }, + { label: "Downloads", path: "~/Downloads" }, + { label: "Documents", path: "~/Documents" }, + { label: "Root", path: "/" }, +]; + type SpecializedViewProps = { model: PreviewModel; parentRef: React.RefObject; @@ -185,27 +194,10 @@ export class PreviewModel implements ViewModel { elemtype: "iconbutton", icon: "folder-open", longClick: (e: React.MouseEvent) => { - const menuItems: ContextMenuItem[] = []; - menuItems.push({ - label: "Go to Home", - click: () => this.goHistory("~"), - }); - menuItems.push({ - label: "Go to Desktop", - click: () => this.goHistory("~/Desktop"), - }); - menuItems.push({ - label: "Go to Downloads", - click: () => this.goHistory("~/Downloads"), - }); - menuItems.push({ - label: "Go to Documents", - click: () => this.goHistory("~/Documents"), - }); - menuItems.push({ - label: "Go to Root", - click: () => this.goHistory("/"), - }); + const menuItems: ContextMenuItem[] = BOOKMARKS.map((bookmark) => ({ + label: `Go to ${bookmark.label} (${bookmark.path})`, + click: () => this.goHistory(bookmark.path), + })); ContextMenuModel.showContextMenu(menuItems, e); }, }; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index bcaa21e7e6..8ba1a08526 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -523,7 +523,6 @@ const ChatWindow = memo( const handleNewMessage = useCallback( throttle(100, (messagesLen: number) => { if (osRef.current?.osInstance()) { - console.log("handleNewMessage", messagesLen, isUserScrolling.current); const { viewport } = osRef.current.osInstance().elements(); if (prevMessagesLenRef.current !== messagesLen || !isUserScrolling.current) { viewport.scrollTo({ From 2bb212686df9171f04b94e3136f84389602b09ef Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 15:08:46 -0800 Subject: [PATCH 06/16] log NewSession calls. have the wsh version command also return os/arch combo to save an additional session. --- pkg/genconn/ssh-impl.go | 2 + pkg/remote/conncontroller/conncontroller.go | 106 ++++++++++++-------- pkg/remote/connutil.go | 54 +++------- pkg/shellexec/shellexec.go | 3 +- pkg/wshrpc/wshserver/wshserver.go | 4 +- 5 files changed, 85 insertions(+), 84 deletions(-) diff --git a/pkg/genconn/ssh-impl.go b/pkg/genconn/ssh-impl.go index 945a54aa50..4e49f9e66a 100644 --- a/pkg/genconn/ssh-impl.go +++ b/pkg/genconn/ssh-impl.go @@ -6,6 +6,7 @@ package genconn import ( "fmt" "io" + "log" "sync" "golang.org/x/crypto/ssh" @@ -41,6 +42,7 @@ type SSHProcessController struct { // MakeSSHCmdClient creates a new instance of SSHCmdClient func MakeSSHCmdClient(client *ssh.Client, cmdSpec CommandSpec) (*SSHProcessController, error) { + log.Printf("SSH-NEWSESSION (cmdclient)\n") session, err := client.NewSession() if err != nil { return nil, fmt.Errorf("failed to create SSH session: %w", err) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 72b0a7789c..2e8dbbd2a2 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -69,11 +69,12 @@ type SSHConn struct { ActiveConnNum int } -var ConnServerCmdTemplate = strings.TrimSpace(` -%s version || echo "not-installed" -read jwt_token -WAVETERM_JWT="$jwt_token" %s connserver -`) +var ConnServerCmdTemplate = strings.TrimSpace( + strings.Join([]string{ + "%s version 2> /dev/null || echo \"not-installed `uname -sm`\"", + "read jwt_token", + "WAVETERM_JWT=\"$jwt_token\" %s connserver", + }, "\n")) func GetAllConnStatus() []wshrpc.ConnStatus { globalLock.Lock() @@ -225,37 +226,47 @@ func (conn *SSHConn) OpenDomainSocketListener(ctx context.Context) error { return nil } -// expects the output of `wsh version` which looks like `wsh v0.10.4` or "not-installed" -// returns (up-to-date, semver, error) +// expects the output of `wsh version` which looks like `wsh v0.10.4` or "not-installed [os] [arch]" +// returns (up-to-date, semver, osArchStr, error) // if not up to date, or error, version might be "" -func IsWshVersionUpToDate(wshVersionLine string) (bool, string, error) { +func IsWshVersionUpToDate(wshVersionLine string) (bool, string, string, error) { wshVersionLine = strings.TrimSpace(wshVersionLine) - if wshVersionLine == "not-installed" { - return false, "", nil + if strings.HasPrefix(wshVersionLine, "not-installed") { + return false, "not-installed", strings.TrimSpace(strings.TrimPrefix(wshVersionLine, "not-installed")), 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, clientVersion, nil + return false, clientVersion, "", nil + } + return true, clientVersion, "", nil +} + +func (conn *SSHConn) getWshPath() string { + config, ok := conn.getConnectionConfig() + if ok && config.ConnWshPath != "" { + return config.ConnWshPath } - return true, clientVersion, nil + return wavebase.RemoteFullWshBinPath } -// returns (needsInstall, clientVersion, error) -func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, error) { +// returns (needsInstall, clientVersion, osArchStr, error) +// if wsh is not installed, the clientVersion will be "not-installed", and it will also return an osArchStr +// if clientVersion is set, then no osArchStr will be returned +func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, 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) + wshPath := conn.getWshPath() rpcCtx := wshrpc.RpcContext{ ClientType: wshrpc.ClientType_ConnServer, Conn: conn.GetName(), @@ -263,49 +274,50 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, 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) } + log.Printf("SSH-NEWSESSION (StartConnServer)\n") 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, clientVersion, err := IsWshVersionUpToDate(versionLine) + isUpToDate, clientVersion, osArchStr, 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) + conn.Infof(ctx, "connserver up-to-date: %v\n", isUpToDate) if !isUpToDate { sshSession.Close() - return true, clientVersion, nil + return true, clientVersion, osArchStr, 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, clientVersion, 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 @@ -351,11 +363,11 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, error) defer cancelFn() err = wshutil.DefaultRouter.WaitForRegister(regCtx, wshutil.MakeConnectionRouteId(rpcCtx.Conn)) if err != nil { - return false, clientVersion, 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, clientVersion, nil + return false, clientVersion, "", nil } type WshInstallOpts struct { @@ -438,17 +450,22 @@ func (conn *SSHConn) getPermissionToInstallWsh(ctx context.Context, clientDispla return true, nil } -func (conn *SSHConn) InstallWsh(ctx context.Context) error { +func (conn *SSHConn) InstallWsh(ctx context.Context, osArchStr string) error { conn.Infof(ctx, "running installWsh...\n") client := conn.GetClient() if client == nil { conn.Infof(ctx, "ERROR ssh client is not connected, cannot install\n") return fmt.Errorf("ssh client is not connected, cannot install") } - clientOs, clientArch, err := remote.GetClientPlatform(ctx, genconn.MakeSSHShellClient(client)) + var clientOs, clientArch string + var err error + if osArchStr != "" { + clientOs, clientArch, err = remote.GetClientPlatformFromOsArchStr(ctx, osArchStr) + } else { + 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, client, clientOs, clientArch) @@ -547,8 +564,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.GetWatcher().GetFullConfig() - existingConnection, ok := existingConfig.Connections[conn.GetName()] + existingConnection, ok := conn.getConnectionConfig() if ok { identityFiles = existingConnection.SshIdentityFile } @@ -592,7 +608,7 @@ 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()] + connSettings, ok := conn.getConnectionConfig() if ok { if connSettings.ConnWshEnabled != nil { enableWsh = *connSettings.ConnWshEnabled @@ -639,7 +655,7 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) err = fmt.Errorf("error opening domain socket listener: %w", err) return WshCheckResult{NoWshReason: "error opening domain socket", WshError: err} } - needsInstall, clientVersion, err := conn.StartConnServer(ctx) + needsInstall, clientVersion, osArchStr, err := conn.StartConnServer(ctx) if err != nil { conn.Infof(ctx, "ERROR starting conn server: %v\n", err) err = fmt.Errorf("error starting conn server: %w", err) @@ -647,13 +663,13 @@ 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, osArchStr) if err != nil { conn.Infof(ctx, "ERROR installing wsh: %v\n", err) err = fmt.Errorf("error installing wsh: %w", err) return WshCheckResult{NoWshReason: "error installing wsh/connserver", WshError: err} } - needsInstall, clientVersion, 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) err = fmt.Errorf("error starting conn server (after install): %w", err) @@ -670,6 +686,15 @@ func (conn *SSHConn) tryEnableWsh(ctx context.Context, clientDisplayName string) } } +func (conn *SSHConn) getConnectionConfig() (wshrpc.ConnKeywords, bool) { + config := wconfig.GetWatcher().GetFullConfig() + connSettings, ok := config.Connections[conn.GetName()] + if !ok { + return wshrpc.ConnKeywords{}, false + } + return connSettings, true +} + func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckResult) { conn.WshEnabled.Store(result.WshEnabled) conn.SetWshError(result.WshError) @@ -677,9 +702,8 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckRes conn.NoWshReason = result.NoWshReason conn.WshVersion = result.ClientVersion }) - config := wconfig.GetWatcher().GetFullConfig() - connSettings, ok := config.Connections[conn.GetName()] - if ok && connSettings.ConnWshEnabled != nil { + connConfig, ok := conn.getConnectionConfig() + if ok && connConfig.ConnWshEnabled != nil { return } meta := make(map[string]any) diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index b79b104745..4aec67e9cd 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -16,6 +16,7 @@ import ( "strings" "text/template" + "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/util/shellutil" "github.com/wavetermdev/waveterm/pkg/wavebase" @@ -35,46 +36,6 @@ func ParseOpts(input string) (*SSHOpts, error) { return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil } -func GetWshPath(client *ssh.Client) string { - defaultPath := wavebase.RemoteFullWshBinPath - session, err := client.NewSession() - if err != nil { - log.Printf("unable to detect client's wsh path. using default. error: %v", err) - return defaultPath - } - - out, whichErr := session.Output("which wsh") - if whichErr == nil { - return strings.TrimSpace(string(out)) - } - - session, err = client.NewSession() - if err != nil { - log.Printf("unable to detect client's wsh path. using default. error: %v", err) - return defaultPath - } - - out, whereErr := session.Output("where.exe wsh") - if whereErr == nil { - return strings.TrimSpace(string(out)) - } - - // check cmd on windows since it requires an absolute path with backslashes - session, err = client.NewSession() - if err != nil { - log.Printf("unable to detect client's wsh path. using default. error: %v", err) - return defaultPath - } - - out, cmdErr := session.Output("(dir 2>&1 *``|echo %userprofile%\\.waveterm%\\.waveterm\\bin\\wsh.exe);&<# rem #>echo none") //todo - if cmdErr == nil && strings.TrimSpace(string(out)) != "none" { - return strings.TrimSpace(string(out)) - } - - // no custom install, use default path - return defaultPath -} - func normalizeOs(os string) string { os = strings.ToLower(strings.TrimSpace(os)) return os @@ -94,6 +55,7 @@ func normalizeArch(arch string) string { // returns (os, arch, error) // guaranteed to return a supported platform func GetClientPlatform(ctx context.Context, shell genconn.ShellClient) (string, string, error) { + blocklogger.Infof(ctx, "[conndebug] running `uname -sm` to detect client platform\n") stdout, stderr, err := genconn.RunSimpleCommand(ctx, shell, genconn.CommandSpec{ Cmd: "uname -sm", }) @@ -112,6 +74,18 @@ func GetClientPlatform(ctx context.Context, shell genconn.ShellClient) (string, return os, arch, nil } +func GetClientPlatformFromOsArchStr(ctx context.Context, osArchStr string) (string, string, error) { + parts := strings.Fields(strings.TrimSpace(osArchStr)) + if len(parts) != 2 { + return "", "", fmt.Errorf("unexpected output from uname: %s", osArchStr) + } + os, arch := normalizeOs(parts[0]), normalizeArch(parts[1]) + if err := wavebase.ValidateWshSupportedArch(os, arch); err != nil { + return "", "", err + } + return os, arch, nil +} + var installTemplateRawDefault = strings.TrimSpace(` mkdir -p {{.installDir}} || exit 1 cat > {{.tempPath}} || exit 1 diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index e68eae2454..55d12d1ff4 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -272,6 +272,7 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() + log.Printf("SSH-NEWSESSION (StartRemoteShellProcNoWsh)\n") session, err := client.NewSession() if err != nil { return nil, err @@ -371,7 +372,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " ")) log.Printf("combined command is: %s", cmdCombined) } - + log.Printf("SSH-NEWSESSION (StartRemoteShellProc)\n") session, err := client.NewSession() if err != nil { return nil, err diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index cb7d9ff646..7473540fea 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -696,7 +696,7 @@ func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, data wshrpc.Co if conn == nil { return fmt.Errorf("connection not found: %s", connName) } - return conn.InstallWsh(ctx) + return conn.InstallWsh(ctx, "") } func (ws *WshServer) ConnUpdateWshCommand(ctx context.Context, remoteInfo wshrpc.RemoteInfo) (bool, error) { @@ -710,7 +710,7 @@ func (ws *WshServer) ConnUpdateWshCommand(ctx context.Context, remoteInfo wshrpc } log.Printf("checking wsh version for connection %s (current: %s)", connName, remoteInfo.ClientVersion) - upToDate, _, err := conncontroller.IsWshVersionUpToDate(remoteInfo.ClientVersion) + upToDate, _, _, err := conncontroller.IsWshVersionUpToDate(remoteInfo.ClientVersion) if err != nil { return false, fmt.Errorf("unable to compare wsh version: %w", err) } From 742545b08dbc4e61284821b848d275e321da812a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 15:29:26 -0800 Subject: [PATCH 07/16] fix writestdout/writestdrr calls to use fmtstr --- cmd/wsh/cmd/wshcmd-rcfiles.go | 2 +- cmd/wsh/cmd/wshcmd-setbg.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-rcfiles.go b/cmd/wsh/cmd/wshcmd-rcfiles.go index 2db2fb76bf..745d325682 100644 --- a/cmd/wsh/cmd/wshcmd-rcfiles.go +++ b/cmd/wsh/cmd/wshcmd-rcfiles.go @@ -19,7 +19,7 @@ var rcfilesCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { err := wshutil.InstallRcFiles() if err != nil { - WriteStderr(err.Error()) + WriteStderr("%s\n", err.Error()) return } }, diff --git a/cmd/wsh/cmd/wshcmd-setbg.go b/cmd/wsh/cmd/wshcmd-setbg.go index 5c05feeafa..ed0b114001 100644 --- a/cmd/wsh/cmd/wshcmd-setbg.go +++ b/cmd/wsh/cmd/wshcmd-setbg.go @@ -162,7 +162,7 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) { if err != nil { return fmt.Errorf("error formatting metadata: %v", err) } - WriteStdout(string(jsonBytes) + "\n") + WriteStdout("%s\n", string(jsonBytes)) return nil } From 261dc57b34dfffaeef52f14a26b5b7b3e54705f9 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 15:29:49 -0800 Subject: [PATCH 08/16] add conn:shellpath override config value --- frontend/types/gotypes.d.ts | 1 + pkg/wshrpc/wshrpctypes.go | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 370d63137c..876e63bd5e 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -304,6 +304,7 @@ declare global { "conn:askbeforewshinstall"?: boolean; "conn:overrideconfig"?: boolean; "conn:wshpath"?: string; + "conn:shellpath"?: string; "display:hidden"?: boolean; "display:order"?: number; "term:*"?: boolean; diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index b2f7d02dc3..28c69e20a0 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -475,6 +475,7 @@ type ConnKeywords struct { ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"` ConnOverrideConfig bool `json:"conn:overrideconfig,omitempty"` ConnWshPath string `json:"conn:wshpath,omitempty"` + ConnShellPath string `json:"conn:shellpath,omitempty"` DisplayHidden *bool `json:"display:hidden,omitempty"` DisplayOrder float32 `json:"display:order,omitempty"` From b127827093917ac903fb6585d3ecb247b28346d5 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 15:30:36 -0800 Subject: [PATCH 09/16] more logging in StartRemoteShellProc, use override shellpath if specified. --- pkg/blockcontroller/blockcontroller.go | 6 ++-- pkg/remote/conncontroller/conncontroller.go | 8 +++++ pkg/shellexec/shellexec.go | 38 ++++++++++++++------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index df1ff1be7a..80039a27d0 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -369,18 +369,18 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta cmdOpts.Env[wshutil.WaveJwtTokenVarName] = jwtStr } if !conn.WshEnabled.Load() { - shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn) + shellProc, err = shellexec.StartRemoteShellProcNoWsh(ctx, rc.TermSize, cmdStr, cmdOpts, conn) if err != nil { return nil, err } } else { - shellProc, err = shellexec.StartRemoteShellProc(rc.TermSize, cmdStr, cmdOpts, conn) + shellProc, err = shellexec.StartRemoteShellProc(ctx, rc.TermSize, cmdStr, cmdOpts, conn) if err != nil { conn.SetWshError(err) conn.WshEnabled.Store(false) log.Printf("error starting remote shell proc with wsh: %v", err) log.Print("attempting install without wsh") - shellProc, err = shellexec.StartRemoteShellProcNoWsh(rc.TermSize, cmdStr, cmdOpts, conn) + shellProc, err = shellexec.StartRemoteShellProcNoWsh(ctx, rc.TermSize, cmdStr, cmdOpts, conn) if err != nil { return nil, err } diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 2e8dbbd2a2..e5fd285e4f 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -254,6 +254,14 @@ func (conn *SSHConn) getWshPath() string { return wavebase.RemoteFullWshBinPath } +func (conn *SSHConn) GetConfigShellPath() string { + config, ok := conn.getConnectionConfig() + if !ok { + return "" + } + return config.ConnShellPath +} + // returns (needsInstall, clientVersion, osArchStr, error) // if wsh is not installed, the clientVersion will be "not-installed", and it will also return an osArchStr // if clientVersion is set, then no osArchStr will be returned diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 55d12d1ff4..aa6a3c09a4 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -270,9 +270,9 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st return &ShellProc{Cmd: cmdWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil } -func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { +func StartRemoteShellProcNoWsh(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() - log.Printf("SSH-NEWSESSION (StartRemoteShellProcNoWsh)\n") + conn.Infof(ctx, "SSH-NEWSESSION (StartRemoteShellProcNoWsh)") session, err := client.NewSession() if err != nil { return nil, err @@ -313,7 +313,7 @@ func StartRemoteShellProcNoWsh(termSize waveobj.TermSize, cmdStr string, cmdOpts return &ShellProc{Cmd: sessionWrap, ConnName: conn.GetName(), CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil } -func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { +func StartRemoteShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType, conn *conncontroller.SSHConn) (*ShellProc, error) { client := conn.GetClient() connRoute := wshutil.MakeConnectionRouteId(conn.GetName()) rpcClient := wshclient.GetBareRpcClient() @@ -322,11 +322,24 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm return nil, fmt.Errorf("unable to obtain client info: %w", err) } log.Printf("client info collected: %+#v", remoteInfo) - - shellPath := cmdOpts.ShellPath - if shellPath == "" { + var shellPath string + if cmdOpts.ShellPath != "" { + conn.Infof(ctx, "using shell path from command opts: %s\n", cmdOpts.ShellPath) + shellPath = cmdOpts.ShellPath + } + configShellPath := conn.GetConfigShellPath() + if shellPath == "" && configShellPath != "" { + conn.Infof(ctx, "using shell path from config (conn:shellpath): %s\n", configShellPath) + shellPath = configShellPath + } + if shellPath == "" && remoteInfo.Shell != "" { + conn.Infof(ctx, "using shell path detected on remote machine: %s\n", remoteInfo.Shell) shellPath = remoteInfo.Shell } + if shellPath == "" { + conn.Infof(ctx, "no shell path detected, using default (/bin/bash)\n") + shellPath = "/bin/bash" + } var shellOpts []string var cmdCombined string log.Printf("detected shell %q for conn %q\n", shellPath, conn.GetName()) @@ -338,11 +351,11 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm } shellOpts = append(shellOpts, cmdOpts.ShellOpts...) shellType := getShellTypeFromShellPath(shellPath) + conn.Infof(ctx, "detected shell type: %s\n", shellType) if cmdStr == "" { /* transform command in order to inject environment vars */ if shellType == ShellType_bash { - log.Printf("recognized as bash shell") // add --rcfile // cant set -l or -i with --rcfile bashPath := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s/.bashrc", shellutil.BashIntegrationDir)) @@ -359,25 +372,24 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm } else { if cmdOpts.Login { shellOpts = append(shellOpts, "-l") - } else if cmdOpts.Interactive { + } + if cmdOpts.Interactive { shellOpts = append(shellOpts, "-i") } // zdotdir setting moved to after session is created } cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " ")) - log.Printf("combined command is: %s", cmdCombined) } else { shellPath = cmdStr shellOpts = append(shellOpts, "-c", cmdStr) cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " ")) - log.Printf("combined command is: %s", cmdCombined) } - log.Printf("SSH-NEWSESSION (StartRemoteShellProc)\n") + conn.Infof(ctx, "starting shell, using command: %s\n", cmdCombined) + conn.Infof(ctx, "SSH-NEWSESSION (StartRemoteShellProc)\n") session, err := client.NewSession() if err != nil { return nil, err } - remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe() if err != nil { return nil, err @@ -410,6 +422,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm if shellType == ShellType_zsh { zshDir := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s", shellutil.ZshIntegrationDir)) + conn.Infof(ctx, "setting ZDOTDIR to %s\n", zshDir) cmdCombined = fmt.Sprintf(`ZDOTDIR=%s %s`, zshDir, cmdCombined) } @@ -418,7 +431,6 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm return nil, fmt.Errorf("no jwt token provided to connection") } cmdCombined = fmt.Sprintf(`%s=%s %s`, wshutil.WaveJwtTokenVarName, jwtToken, cmdCombined) - session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil) sessionWrap := MakeSessionWrap(session, cmdCombined, pipePty) err = sessionWrap.Start() From 491476b8793c964262df1b10b65ff41da3fd56f0 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 18:42:39 -0800 Subject: [PATCH 10/16] update logging --- pkg/remote/conncontroller/conncontroller.go | 2 +- pkg/remote/connutil.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index e5fd285e4f..8f22d26c80 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -284,7 +284,7 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, string, if err != nil { return false, "", "", fmt.Errorf("unable to create jwt token for conn controller: %w", err) } - log.Printf("SSH-NEWSESSION (StartConnServer)\n") + conn.Infof(ctx, "SSH-NEWSESSION (StartConnServer)\n") sshSession, err := client.NewSession() if err != nil { return false, "", "", fmt.Errorf("unable to create ssh session for conn controller: %w", err) diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index 4aec67e9cd..dfbee47681 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -106,13 +106,14 @@ func CpWshToRemote(ctx context.Context, client *ssh.Client, clientOs string, cli defer input.Close() installWords := map[string]string{ "installDir": filepath.ToSlash(filepath.Dir(wavebase.RemoteFullWshBinPath)), - "tempPath": filepath.ToSlash(wavebase.RemoteFullWshBinPath + ".temp"), - "installPath": filepath.ToSlash(wavebase.RemoteFullWshBinPath), + "tempPath": wavebase.RemoteFullWshBinPath + ".temp", + "installPath": wavebase.RemoteFullWshBinPath, } var installCmd bytes.Buffer if err := installTemplate.Execute(&installCmd, installWords); err != nil { return fmt.Errorf("failed to prepare install command: %w", err) } + blocklogger.Infof(ctx, "[conndebug] copying %q to remote server %q\n", wshLocalPath, wavebase.RemoteFullWshBinPath) genCmd, err := genconn.MakeSSHCmdClient(client, genconn.CommandSpec{ Cmd: installCmd.String(), }) From 34b11446a02560fb7e2d82ba9bc9c36636e2bd47 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 22:07:22 -0800 Subject: [PATCH 11/16] hardquote for powershell --- pkg/genconn/quote.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/genconn/quote.go b/pkg/genconn/quote.go index ad21910eab..0bef3ad446 100644 --- a/pkg/genconn/quote.go +++ b/pkg/genconn/quote.go @@ -6,7 +6,8 @@ package genconn import "regexp" var ( - safePattern = regexp.MustCompile(`^[a-zA-Z0-9_/.-]+$`) + safePattern = regexp.MustCompile(`^[a-zA-Z0-9_/.-]+$`) + psSafePattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) needsEscape = map[byte]bool{ '"': true, @@ -39,6 +40,32 @@ func HardQuote(s string) string { return string(buf) } +func HardQuotePowerShell(s string) string { + if s == "" { + return "\"\"" + } + + if psSafePattern.MatchString(s) { + return s + } + + buf := make([]byte, 0, len(s)+5) + buf = append(buf, '"') + + for i := 0; i < len(s); i++ { + c := s[i] + // In PowerShell, backtick (`) is the escape character + switch c { + case '"', '`', '$': + buf = append(buf, '`') + } + buf = append(buf, c) + } + + buf = append(buf, '"') + return string(buf) +} + func SoftQuote(s string) string { if s == "" { return "\"\"" From bef6d15212cb60c2a8f8244d0b8c690ae01c02a4 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 22:38:02 -0800 Subject: [PATCH 12/16] add fish initialization script (wave.fish). make InitRcFiles more consistent -- requires an absolute path. hardquote path on go side (for posix and pwsh), so no quotes in the actual files. remote quoting for tilde paths --- pkg/shellexec/shellexec.go | 18 +++++--- pkg/util/shellutil/shellutil.go | 74 ++++++++++++++++++++++----------- pkg/wshutil/wshutil.go | 4 +- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index aa6a3c09a4..8903dfd535 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -19,7 +19,6 @@ import ( "time" "github.com/creack/pty" - "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" @@ -358,14 +357,18 @@ func StartRemoteShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr if shellType == ShellType_bash { // add --rcfile // cant set -l or -i with --rcfile - bashPath := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s/.bashrc", shellutil.BashIntegrationDir)) + bashPath := fmt.Sprintf("~/.waveterm/%s/.bashrc", shellutil.BashIntegrationDir) shellOpts = append(shellOpts, "--rcfile", bashPath) } else if shellType == ShellType_fish { - fishDir := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s", shellutil.WaveHomeBinDir)) - carg := fmt.Sprintf(`"set -x PATH %s $PATH"`, fishDir) + if cmdOpts.Login { + shellOpts = append(shellOpts, "-l") + } + // source the wave.fish file + waveFishPath := fmt.Sprintf("~/.waveterm/%s/wave.fish", shellutil.FishIntegrationDir) + carg := fmt.Sprintf(`"source %s"`, waveFishPath) shellOpts = append(shellOpts, "-C", carg) } else if shellType == ShellType_pwsh { - pwshPath := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir)) + pwshPath := fmt.Sprintf("~/.waveterm/%s/wavepwsh.ps1", shellutil.PwshIntegrationDir) // powershell is weird about quoted path executables and requires an ampersand first shellPath = "& " + shellPath shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", pwshPath) @@ -421,7 +424,7 @@ func StartRemoteShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr } if shellType == ShellType_zsh { - zshDir := genconn.SoftQuote(fmt.Sprintf("~/.waveterm/%s", shellutil.ZshIntegrationDir)) + zshDir := fmt.Sprintf("~/.waveterm/%s", shellutil.ZshIntegrationDir) conn.Infof(ctx, "setting ZDOTDIR to %s\n", zshDir) cmdCombined = fmt.Sprintf(`ZDOTDIR=%s %s`, zshDir, cmdCombined) } @@ -474,6 +477,9 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt // cant set -l or -i with --rcfile shellOpts = append(shellOpts, "--rcfile", shellutil.GetBashRcFileOverride()) } else if isFishShell(shellPath) { + if cmdOpts.Login { + shellOpts = append(shellOpts, "-l") + } wshBinDir := filepath.Join(wavebase.GetWaveDataDir(), shellutil.WaveHomeBinDir) quotedWshBinDir := utilfn.ShellQuote(wshBinDir, false, 300) shellOpts = append(shellOpts, "-C", fmt.Sprintf("set -x PATH %s $PATH", quotedWshBinDir)) diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index debdf8b90e..e6a975bf8f 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" @@ -33,9 +34,11 @@ var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`) const DefaultShellPath = "/bin/bash" const ( + // there must be no spaces in these integration dir paths ZshIntegrationDir = "shell/zsh" BashIntegrationDir = "shell/bash" PwshIntegrationDir = "shell/pwsh" + FishIntegrationDir = "shell/fish" WaveHomeBinDir = "bin" ZshStartup_Zprofile = ` @@ -102,11 +105,22 @@ if type _init_completion &>/dev/null; then fi ` + + FishStartup_Wavefish = ` +# this file is sourced with -C +# Add Wave binary directory to PATH +set -x PATH {{.WSHBINDIR}} $PATH + +# Load Wave completions +wsh completion fish | source +` + PwshStartup_wavepwsh = ` -# no need to source regular profiles since we cannot -# overwrite those with powershell. Instead we will source -# this file with -NoExit -$env:PATH = "{{.WSHBINDIR}}" + "{{.PATHSEP}}" + $env:PATH +# We source this file with -NoExit -File +$env:PATH = {{.WSHBINDIR_PWSH}} + "{{.PATHSEP}}" + $env:PATH + +# Load Wave completions +wsh completion powershell | Out-String | Invoke-Expression ` ) @@ -256,8 +270,10 @@ func GetWshBinaryPath(version string, goos string, goarch string) (string, error return filepath.Join(wavebase.GetWaveAppBinPath(), baseName), nil } -func InitRcFiles(waveHome string, wshBinDir string) error { - // ensure directiries exist +// absWshBinDir must be an absolute, expanded path (no ~ or $HOME, etc.) +// it will be hard-quoted appropriately for the shell +func InitRcFiles(waveHome string, absWshBinDir string) error { + // ensure directories exist zshDir := filepath.Join(waveHome, ZshIntegrationDir) err := wavebase.CacheEnsureDir(zshDir, ZshIntegrationDir, 0755, ZshIntegrationDir) if err != nil { @@ -268,43 +284,55 @@ func InitRcFiles(waveHome string, wshBinDir string) error { if err != nil { return err } + fishDir := filepath.Join(waveHome, FishIntegrationDir) + err = wavebase.CacheEnsureDir(fishDir, FishIntegrationDir, 0755, FishIntegrationDir) + if err != nil { + return err + } pwshDir := filepath.Join(waveHome, PwshIntegrationDir) err = wavebase.CacheEnsureDir(pwshDir, PwshIntegrationDir, 0755, PwshIntegrationDir) if err != nil { return err } + var pathSep string + if runtime.GOOS == "windows" { + pathSep = ";" + } else { + pathSep = ":" + } + params := map[string]string{ + "WSHBINDIR": genconn.HardQuote(absWshBinDir), + "WSHBINDIR_PWSH": genconn.HardQuotePowerShell(absWshBinDir), + "PATHSEP": pathSep, + } + // write files to directory - zprofilePath := filepath.Join(zshDir, ".zprofile") - err = os.WriteFile(zprofilePath, []byte(ZshStartup_Zprofile), 0644) + err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zprofile"), ZshStartup_Zprofile, params) if err != nil { return fmt.Errorf("error writing zsh-integration .zprofile: %v", err) } - err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshrc"), ZshStartup_Zshrc, map[string]string{"WSHBINDIR": fmt.Sprintf(`"%s"`, wshBinDir)}) + err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshrc"), ZshStartup_Zshrc, params) if err != nil { return fmt.Errorf("error writing zsh-integration .zshrc: %v", err) } - zloginPath := filepath.Join(zshDir, ".zlogin") - err = os.WriteFile(zloginPath, []byte(ZshStartup_Zlogin), 0644) + err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zlogin"), ZshStartup_Zlogin, params) if err != nil { return fmt.Errorf("error writing zsh-integration .zlogin: %v", err) } - zshenvPath := filepath.Join(zshDir, ".zshenv") - err = os.WriteFile(zshenvPath, []byte(ZshStartup_Zshenv), 0644) + err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshenv"), ZshStartup_Zshenv, params) if err != nil { return fmt.Errorf("error writing zsh-integration .zshenv: %v", err) } - err = utilfn.WriteTemplateToFile(filepath.Join(bashDir, ".bashrc"), BashStartup_Bashrc, map[string]string{"WSHBINDIR": fmt.Sprintf(`"%s"`, wshBinDir)}) + err = utilfn.WriteTemplateToFile(filepath.Join(bashDir, ".bashrc"), BashStartup_Bashrc, params) if err != nil { return fmt.Errorf("error writing bash-integration .bashrc: %v", err) } - var pathSep string - if runtime.GOOS == "windows" { - pathSep = ";" - } else { - pathSep = ":" + err = utilfn.WriteTemplateToFile(filepath.Join(fishDir, "wave.fish"), FishStartup_Wavefish, params) + if err != nil { + return fmt.Errorf("error writing fish-integration wave.fish: %v", err) } - err = utilfn.WriteTemplateToFile(filepath.Join(pwshDir, "wavepwsh.ps1"), PwshStartup_wavepwsh, map[string]string{"WSHBINDIR": toPwshEnvVarRef(wshBinDir), "PATHSEP": pathSep}) + err = utilfn.WriteTemplateToFile(filepath.Join(pwshDir, "wavepwsh.ps1"), PwshStartup_wavepwsh, params) if err != nil { return fmt.Errorf("error writing pwsh-integration wavepwsh.ps1: %v", err) } @@ -316,7 +344,7 @@ func initCustomShellStartupFilesInternal() error { log.Printf("initializing wsh and shell startup files\n") waveDataHome := wavebase.GetWaveDataDir() binDir := filepath.Join(waveDataHome, WaveHomeBinDir) - err := InitRcFiles(waveDataHome, `$WAVETERM_WSHBINDIR`) + err := InitRcFiles(waveDataHome, binDir) if err != nil { return err } @@ -347,7 +375,3 @@ func initCustomShellStartupFilesInternal() error { log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath) return nil } - -func toPwshEnvVarRef(input string) string { - return strings.Replace(input, "$", "$env:", -1) -} diff --git a/pkg/wshutil/wshutil.go b/pkg/wshutil/wshutil.go index 77342fcd6d..6075a50dd8 100644 --- a/pkg/wshutil/wshutil.go +++ b/pkg/wshutil/wshutil.go @@ -566,6 +566,6 @@ func GetInfo() wshrpc.RemoteInfo { func InstallRcFiles() error { home := wavebase.GetHomeDir() waveDir := filepath.Join(home, wavebase.RemoteWaveHomeDirName) - winBinDir := filepath.Join(waveDir, wavebase.RemoteWshBinDirName) - return shellutil.InitRcFiles(waveDir, winBinDir) + wshBinDir := filepath.Join(waveDir, wavebase.RemoteWshBinDirName) + return shellutil.InitRcFiles(waveDir, wshBinDir) } From 0e24e963f830f6865e870ea23a4de84e96866619 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 13 Jan 2025 22:50:33 -0800 Subject: [PATCH 13/16] modify some of the local shell setup to fix same issues with ssh remote setup. rename some functions to make it more clear they are for local use only --- pkg/blockcontroller/blockcontroller.go | 2 +- pkg/remote/connutil.go | 2 +- pkg/shellexec/shellexec.go | 29 ++++++++++++++------------ pkg/util/shellutil/shellutil.go | 14 ++++++++----- pkg/wsl/wsl.go | 2 +- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 80039a27d0..08a75a47af 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -408,7 +408,7 @@ func (bc *BlockController) setupAndStartShellProcess(rc *RunShellOpts, blockMeta if len(blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)) > 0 { cmdOpts.ShellOpts = append([]string{}, blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)...) } - shellProc, err = shellexec.StartShellProc(rc.TermSize, cmdStr, cmdOpts) + shellProc, err = shellexec.StartLocalShellProc(rc.TermSize, cmdStr, cmdOpts) if err != nil { return nil, err } diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index dfbee47681..d9ea4153b5 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -95,7 +95,7 @@ chmod a+x {{.installPath}} || exit 1 var installTemplate = template.Must(template.New("wsh-install-template").Parse(installTemplateRawDefault)) func CpWshToRemote(ctx context.Context, client *ssh.Client, clientOs string, clientArch string) error { - wshLocalPath, err := shellutil.GetWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch) + wshLocalPath, err := shellutil.GetLocalWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch) if err != nil { return err } diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 8903dfd535..545d5e627d 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -19,12 +19,12 @@ import ( "time" "github.com/creack/pty" + "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" "github.com/wavetermdev/waveterm/pkg/util/pamparse" "github.com/wavetermdev/waveterm/pkg/util/shellutil" - "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wshrpc" @@ -383,6 +383,7 @@ func StartRemoteShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr } cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " ")) } else { + // TODO check quoting of cmdStr shellPath = cmdStr shellOpts = append(shellOpts, "-c", cmdStr) cmdCombined = fmt.Sprintf("%s %s", shellPath, strings.Join(shellOpts, " ")) @@ -462,7 +463,7 @@ func isFishShell(shellPath string) bool { return strings.Contains(shellBase, "fish") } -func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType) (*ShellProc, error) { +func StartLocalShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOptsType) (*ShellProc, error) { shellutil.InitCustomShellStartupFiles() var ecmd *exec.Cmd var shellOpts []string @@ -470,32 +471,34 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt if shellPath == "" { shellPath = shellutil.DetectLocalShellPath() } + shellType := getShellTypeFromShellPath(shellPath) shellOpts = append(shellOpts, cmdOpts.ShellOpts...) if cmdStr == "" { - if isBashShell(shellPath) { + if shellType == ShellType_bash { // add --rcfile // cant set -l or -i with --rcfile - shellOpts = append(shellOpts, "--rcfile", shellutil.GetBashRcFileOverride()) - } else if isFishShell(shellPath) { + shellOpts = append(shellOpts, "--rcfile", shellutil.GetLocalBashRcFileOverride()) + } else if shellType == ShellType_fish { if cmdOpts.Login { shellOpts = append(shellOpts, "-l") } - wshBinDir := filepath.Join(wavebase.GetWaveDataDir(), shellutil.WaveHomeBinDir) - quotedWshBinDir := utilfn.ShellQuote(wshBinDir, false, 300) - shellOpts = append(shellOpts, "-C", fmt.Sprintf("set -x PATH %s $PATH", quotedWshBinDir)) - } else if remote.IsPowershell(shellPath) { - shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", shellutil.GetWavePowershellEnv()) + waveFishPath := shellutil.GetLocalWaveFishFilePath() + carg := fmt.Sprintf("source %s", genconn.HardQuote(waveFishPath)) + shellOpts = append(shellOpts, "-C", carg) + } else if shellType == ShellType_pwsh { + shellOpts = append(shellOpts, "-ExecutionPolicy", "Bypass", "-NoExit", "-File", shellutil.GetLocalWavePowershellEnv()) } else { if cmdOpts.Login { shellOpts = append(shellOpts, "-l") - } else if cmdOpts.Interactive { + } + if cmdOpts.Interactive { shellOpts = append(shellOpts, "-i") } } ecmd = exec.Command(shellPath, shellOpts...) ecmd.Env = os.Environ() - if isZshShell(shellPath) { - shellutil.UpdateCmdEnv(ecmd, map[string]string{"ZDOTDIR": shellutil.GetZshZDotDir()}) + if shellType == ShellType_zsh { + shellutil.UpdateCmdEnv(ecmd, map[string]string{"ZDOTDIR": shellutil.GetLocalZshZDotDir()}) } } else { shellOpts = append(shellOpts, "-c", cmdStr) diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index e6a975bf8f..f89568bcfe 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -240,19 +240,23 @@ func InitCustomShellStartupFiles() error { return err } -func GetBashRcFileOverride() string { +func GetLocalBashRcFileOverride() string { return filepath.Join(wavebase.GetWaveDataDir(), BashIntegrationDir, ".bashrc") } -func GetWavePowershellEnv() string { +func GetLocalWaveFishFilePath() string { + return filepath.Join(wavebase.GetWaveDataDir(), FishIntegrationDir, "wave.fish") +} + +func GetLocalWavePowershellEnv() string { return filepath.Join(wavebase.GetWaveDataDir(), PwshIntegrationDir, "wavepwsh.ps1") } -func GetZshZDotDir() string { +func GetLocalZshZDotDir() string { return filepath.Join(wavebase.GetWaveDataDir(), ZshIntegrationDir) } -func GetWshBinaryPath(version string, goos string, goarch string) (string, error) { +func GetLocalWshBinaryPath(version string, goos string, goarch string) (string, error) { ext := "" if goarch == "amd64" { goarch = "x64" @@ -355,7 +359,7 @@ func initCustomShellStartupFilesInternal() error { } // copy the correct binary to bin - wshFullPath, err := GetWshBinaryPath(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH) + wshFullPath, err := GetLocalWshBinaryPath(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH) if err != nil { log.Printf("error (non-fatal), could not resolve wsh binary path: %v\n", err) } diff --git a/pkg/wsl/wsl.go b/pkg/wsl/wsl.go index 47c6b3cf1a..ab40fe54bb 100644 --- a/pkg/wsl/wsl.go +++ b/pkg/wsl/wsl.go @@ -337,7 +337,7 @@ func (conn *WslConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s return err } // attempt to install extension - wshLocalPath, err := shellutil.GetWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch) + wshLocalPath, err := shellutil.GetLocalWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch) if err != nil { return err } From 19f0ed22ea3875d490606b23e9b572a1ddd03992 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Jan 2025 13:59:07 -0800 Subject: [PATCH 14/16] handle newline --- pkg/genconn/quote.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/genconn/quote.go b/pkg/genconn/quote.go index 0bef3ad446..0b8992c9e4 100644 --- a/pkg/genconn/quote.go +++ b/pkg/genconn/quote.go @@ -8,13 +8,6 @@ import "regexp" var ( safePattern = regexp.MustCompile(`^[a-zA-Z0-9_/.-]+$`) psSafePattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) - - needsEscape = map[byte]bool{ - '"': true, - '\\': true, - '$': true, - '`': true, - } ) func HardQuote(s string) string { @@ -30,10 +23,14 @@ func HardQuote(s string) string { buf = append(buf, '"') for i := 0; i < len(s); i++ { - if needsEscape[s[i]] { - buf = append(buf, '\\') + switch s[i] { + case '"', '\\', '$', '`': + buf = append(buf, '\\', s[i]) + case '\n': + buf = append(buf, '\\', '\n') + default: + buf = append(buf, s[i]) } - buf = append(buf, s[i]) } buf = append(buf, '"') @@ -58,6 +55,8 @@ func HardQuotePowerShell(s string) string { switch c { case '"', '`', '$': buf = append(buf, '`') + case '\n': + buf = append(buf, '`', 'n') // PowerShell uses `n for newline } buf = append(buf, c) } From 9fafb656259b4f714d5df3845e68e3143fdc9c7d Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Jan 2025 14:00:02 -0800 Subject: [PATCH 15/16] add semicolons and newlines for this to work in fish --- pkg/remote/conncontroller/conncontroller.go | 7 ++++--- pkg/remote/connutil.go | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 8f22d26c80..27bd9b7d4b 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -71,8 +71,8 @@ type SSHConn struct { var ConnServerCmdTemplate = strings.TrimSpace( strings.Join([]string{ - "%s version 2> /dev/null || echo \"not-installed `uname -sm`\"", - "read jwt_token", + "%s version 2> /dev/null || (echo -n \"not-installed \"; uname -sm);", + "read jwt_token;", "WAVETERM_JWT=\"$jwt_token\" %s connserver", }, "\n")) @@ -297,8 +297,9 @@ func (conn *SSHConn) StartConnServer(ctx context.Context) (bool, string, string, 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) + log.Printf("starting conn controller: %q\n", cmdStr) shWrappedCmdStr := fmt.Sprintf("sh -c %s", genconn.HardQuote(cmdStr)) + blocklogger.Debugf(ctx, "[conndebug] wrapped command:\n%s\n", shWrappedCmdStr) err = sshSession.Start(shWrappedCmdStr) if err != nil { return false, "", "", fmt.Errorf("unable to start conn controller command: %w", err) diff --git a/pkg/remote/connutil.go b/pkg/remote/connutil.go index d9ea4153b5..2407cbf5a2 100644 --- a/pkg/remote/connutil.go +++ b/pkg/remote/connutil.go @@ -87,10 +87,10 @@ func GetClientPlatformFromOsArchStr(ctx context.Context, osArchStr string) (stri } var installTemplateRawDefault = strings.TrimSpace(` -mkdir -p {{.installDir}} || exit 1 -cat > {{.tempPath}} || exit 1 -mv {{.tempPath}} {{.installPath}} || exit 1 -chmod a+x {{.installPath}} || exit 1 +mkdir -p {{.installDir}} || exit 1; +cat > {{.tempPath}} || exit 1; +mv {{.tempPath}} {{.installPath}} || exit 1; +chmod a+x {{.installPath}} || exit 1; `) var installTemplate = template.Must(template.New("wsh-install-template").Parse(installTemplateRawDefault)) From 59198f89147fce70da1be552121074041cfb4bdd Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 14 Jan 2025 14:07:35 -0800 Subject: [PATCH 16/16] add todo comment around fish issue with hardquote --- pkg/genconn/quote.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/genconn/quote.go b/pkg/genconn/quote.go index 0b8992c9e4..469359cbfa 100644 --- a/pkg/genconn/quote.go +++ b/pkg/genconn/quote.go @@ -10,6 +10,9 @@ var ( psSafePattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`) ) +// TODO: fish quoting is slightly different +// specifically \` will cause an inconsistency between fish and bash/zsh :/ +// might need a specific fish quoting function, and an explicit fish shell detection func HardQuote(s string) string { if s == "" { return "\"\""