mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
Big simplification. Remove the FileShare interface that abstracted wsh://, s3://, and wavefile:// files. It produced a lot of complexity for very little usage. We're just going to focus on the wsh:// implementation since that's core to our remote workflows. * remove s3 implementation (and connections, and picker items for preview) * remove capabilities for FE * remove wavefile backend impl as well * simplify wsh file remote backend * remove ability to copy/move/ls recursively * limit file transfers to 32m the longer term fix here is to use the new streaming RPC primitives. they have full end-to-end flow-control built in and will not create pipeline stalls, blocking other requests, and OOM issues. these other impls had to be removed (or fixed) because transferring large files could cause stalls or crashes with the new router infrastructure.
201 lines
6 KiB
Go
201 lines
6 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package remote
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/blocklogger"
|
|
"github.com/wavetermdev/waveterm/pkg/genconn"
|
|
"github.com/wavetermdev/waveterm/pkg/util/iterfn"
|
|
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`)
|
|
|
|
func ParseOpts(input string) (*SSHOpts, error) {
|
|
m := userHostRe.FindStringSubmatch(input)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("invalid format of user@host argument")
|
|
}
|
|
remoteUser, remoteHost, remotePort := m[1], m[2], m[3]
|
|
remoteUser = strings.Trim(remoteUser, "@")
|
|
|
|
return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil
|
|
}
|
|
|
|
func normalizeOs(os string) string {
|
|
os = strings.ToLower(strings.TrimSpace(os))
|
|
return os
|
|
}
|
|
|
|
func normalizeArch(arch string) string {
|
|
arch = strings.ToLower(strings.TrimSpace(arch))
|
|
switch arch {
|
|
case "x86_64", "amd64":
|
|
arch = "x64"
|
|
case "arm64", "aarch64":
|
|
arch = "arm64"
|
|
}
|
|
return arch
|
|
}
|
|
|
|
// 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",
|
|
})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error running uname -sm: %w, stderr: %s", err, stderr)
|
|
}
|
|
// Parse and normalize output
|
|
parts := strings.Fields(strings.ToLower(strings.TrimSpace(stdout)))
|
|
if len(parts) != 2 {
|
|
return "", "", fmt.Errorf("unexpected output from uname: %s", stdout)
|
|
}
|
|
os, arch := normalizeOs(parts[0]), normalizeArch(parts[1])
|
|
if err := wavebase.ValidateWshSupportedArch(os, arch); err != nil {
|
|
return "", "", err
|
|
}
|
|
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;
|
|
mv {{.tempPath}} {{.installPath}} || exit 1;
|
|
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 {
|
|
deadline, ok := ctx.Deadline()
|
|
if ok {
|
|
blocklogger.Debugf(ctx, "[conndebug] CpWshToRemote, timeout: %v\n", time.Until(deadline))
|
|
}
|
|
wshLocalPath, err := shellutil.GetLocalWshBinaryPath(wavebase.WaveVersion, clientOs, clientArch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input, err := os.Open(wshLocalPath)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot open local file %s: %w", wshLocalPath, err)
|
|
}
|
|
defer input.Close()
|
|
installWords := map[string]string{
|
|
"installDir": filepath.ToSlash(filepath.Dir(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(),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create remote command: %w", err)
|
|
}
|
|
stdin, err := genCmd.StdinPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get stdin pipe: %w", err)
|
|
}
|
|
defer stdin.Close()
|
|
stderrBuf, err := genconn.MakeStderrSyncBuffer(genCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get stderr pipe: %w", err)
|
|
}
|
|
if err := genCmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start remote command: %w", err)
|
|
}
|
|
copyDone := make(chan error, 1)
|
|
go func() {
|
|
defer close(copyDone)
|
|
defer stdin.Close()
|
|
if _, err := io.Copy(stdin, input); err != nil && err != io.EOF {
|
|
copyDone <- fmt.Errorf("failed to copy data: %w", err)
|
|
} else {
|
|
copyDone <- nil
|
|
}
|
|
}()
|
|
procErr := genconn.ProcessContextWait(ctx, genCmd)
|
|
if procErr != nil {
|
|
return fmt.Errorf("remote command failed: %w (stderr: %s)", procErr, stderrBuf.String())
|
|
}
|
|
copyErr := <-copyDone
|
|
if copyErr != nil {
|
|
return fmt.Errorf("failed to copy data: %w (stderr: %s)", copyErr, stderrBuf.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsPowershell(shellPath string) bool {
|
|
// get the base path, and then check contains
|
|
shellBase := filepath.Base(shellPath)
|
|
return strings.Contains(shellBase, "powershell") || strings.Contains(shellBase, "pwsh")
|
|
}
|
|
|
|
func NormalizeConfigPattern(pattern string) string {
|
|
userName, err := WaveSshConfigUserSettings().GetStrict(pattern, "User")
|
|
if err != nil || userName == "" {
|
|
log.Printf("warning: error parsing username of %s for conn dropdown: %v", pattern, err)
|
|
localUser, err := user.Current()
|
|
if err == nil {
|
|
userName = localUser.Username
|
|
}
|
|
}
|
|
port, err := WaveSshConfigUserSettings().GetStrict(pattern, "Port")
|
|
if err != nil {
|
|
port = "22"
|
|
}
|
|
if userName != "" {
|
|
userName += "@"
|
|
}
|
|
if port == "22" {
|
|
port = ""
|
|
} else {
|
|
port = ":" + port
|
|
}
|
|
return fmt.Sprintf("%s%s%s", userName, pattern, port)
|
|
}
|
|
|
|
func ParseProfiles() []string {
|
|
connfile, cerrs := wconfig.ReadWaveHomeConfigFile(wconfig.ProfilesFile)
|
|
if len(cerrs) > 0 {
|
|
log.Printf("error reading config file: %v", cerrs[0])
|
|
return nil
|
|
}
|
|
|
|
return iterfn.MapKeysToSorted(connfile)
|
|
}
|