waveterm/pkg/remote/connutil.go
Evan Simkowitz 0413b240dd
Only copy the relevant wavesrv binary when packaging for a specific architecture (#316)
This change shaves ~20 MB off the download size by only copying over the
wavesrv binary that is relevant for whichever architecture we're
currently packaging. This is only relevant for macOS at the moment,
though it can also apply to Windows when we get multi-arch builds
working.

This required renaming our Go binaries from .amd64 to .x64 to comply
with electron-builder's naming conventions.
2024-09-04 11:23:39 -07:00

336 lines
8.1 KiB
Go

package remote
import (
"bytes"
"fmt"
"html/template"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/crypto/ssh"
)
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-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, remotePortStr := m[1], m[2], m[3]
remoteUser = strings.Trim(remoteUser, "@")
var remotePort int
if remotePortStr != "" {
var err error
remotePort, err = strconv.Atoi(remotePortStr)
if err != nil {
return nil, fmt.Errorf("invalid port specified on user@host argument")
}
}
return &SSHOpts{SSHHost: remoteHost, SSHUser: remoteUser, SSHPort: remotePort}, nil
}
func DetectShell(client *ssh.Client) (string, error) {
wshPath := GetWshPath(client)
session, err := client.NewSession()
if err != nil {
return "", err
}
log.Printf("shell detecting using command: %s shell", wshPath)
out, err := session.Output(wshPath + " shell")
if err != nil {
log.Printf("unable to determine shell. defaulting to /bin/bash: %s", err)
return "/bin/bash", nil
}
log.Printf("detecting shell: %s", out)
return fmt.Sprintf(`"%s"`, strings.TrimSpace(string(out))), nil
}
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
}
return strings.TrimSpace(string(out)), nil
}
func GetWshPath(client *ssh.Client) string {
defaultPath := "~/.waveterm/bin/wsh"
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 hasBashInstalled(client *ssh.Client) (bool, error) {
session, err := client.NewSession()
if err != nil {
// this is a true error that should stop further progress
return false, err
}
out, whichErr := session.Output("which bash")
if whichErr == nil && len(out) != 0 {
return true, nil
}
session, err = client.NewSession()
if err != nil {
// this is a true error that should stop further progress
return false, err
}
out, whereErr := session.Output("where.exe bash")
if whereErr == nil && len(out) != 0 {
return true, nil
}
// note: we could also check in /bin/bash explicitly
// just in case that wasn't added to the path. but if
// that's true, we will most likely have worse
// problems going forward
return false, nil
}
func GetClientOs(client *ssh.Client) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", err
}
out, unixErr := session.Output("uname -s")
if unixErr == nil {
formatted := strings.ToLower(string(out))
formatted = strings.TrimSpace(formatted)
return formatted, nil
}
session, err = client.NewSession()
if err != nil {
return "", err
}
out, cmdErr := session.Output("echo %OS%")
if cmdErr == nil {
formatted := strings.ToLower(string(out))
formatted = strings.TrimSpace(formatted)
return strings.Split(formatted, "_")[0], nil
}
session, err = client.NewSession()
if err != nil {
return "", err
}
out, psErr := session.Output("echo $env:OS")
if psErr == nil {
formatted := strings.ToLower(string(out))
formatted = strings.TrimSpace(formatted)
return strings.Split(formatted, "_")[0], nil
}
return "", fmt.Errorf("unable to determine os: {unix: %s, cmd: %s, powershell: %s}", unixErr, cmdErr, psErr)
}
func GetClientArch(client *ssh.Client) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", err
}
out, unixErr := session.Output("uname -m")
if unixErr == nil {
formatted := strings.ToLower(string(out))
formatted = strings.TrimSpace(formatted)
if formatted == "x86_64" {
return "x64", nil
}
return formatted, nil
}
session, err = client.NewSession()
if err != nil {
return "", err
}
out, cmdErr := session.Output("echo %PROCESSOR_ARCHITECTURE%")
if cmdErr == nil {
formatted := strings.ToLower(string(out))
return strings.TrimSpace(formatted), nil
}
session, err = client.NewSession()
if err != nil {
return "", err
}
out, psErr := session.Output("echo $env:PROCESSOR_ARCHITECTURE")
if psErr == nil {
formatted := strings.ToLower(string(out))
return strings.TrimSpace(formatted), nil
}
return "", fmt.Errorf("unable to determine architecture: {unix: %s, cmd: %s, powershell: %s}", unixErr, cmdErr, psErr)
}
var installTemplateRawBash = `bash -c ' \
mkdir -p {{.installDir}}; \
cat > {{.tempPath}}; \
mv {{.tempPath}} {{.installPath}}; \
chmod a+x {{.installPath}};' \
`
var installTemplateRawDefault = ` \
mkdir -p {{.installDir}}; \
cat > {{.tempPath}}; \
mv {{.tempPath}} {{.installPath}}; \
chmod a+x {{.installPath}}; \
`
func CpHostToRemote(client *ssh.Client, sourcePath string, destPath string) error {
// warning: does not work on windows remote yet
bashInstalled, err := hasBashInstalled(client)
if err != nil {
return err
}
var selectedTemplateRaw string
if bashInstalled {
selectedTemplateRaw = installTemplateRawBash
} else {
log.Printf("bash is not installed on remote. attempting with default shell")
selectedTemplateRaw = installTemplateRawDefault
}
var installWords = map[string]string{
"installDir": filepath.Dir(destPath),
"tempPath": destPath + ".temp",
"installPath": destPath,
}
installCmd := &bytes.Buffer{}
installTemplate := template.Must(template.New("").Parse(selectedTemplateRaw))
installTemplate.Execute(installCmd, installWords)
session, err := client.NewSession()
if err != nil {
return err
}
installStdin, err := session.StdinPipe()
if err != nil {
return err
}
err = session.Start(installCmd.String())
if err != nil {
return err
}
input, err := os.Open(sourcePath)
if err != nil {
return fmt.Errorf("cannot open local file %s to send to host: %v", sourcePath, err)
}
go func() {
io.Copy(installStdin, input)
session.Close() // this allows the command to complete for reasons i don't fully understand
}()
return session.Wait()
}
func InstallClientRcFiles(client *ssh.Client) error {
path := GetWshPath(client)
log.Printf("path to wsh searched is: %s", path)
log.Printf("in bytes is: %v", []byte(path))
log.Printf("in bytes expected would be: %v", []byte("~/.waveterm/bin/wsh"))
session, err := client.NewSession()
if err != nil {
// this is a true error that should stop further progress
return err
}
_, err = session.Output(path + " rcfiles")
return err
}
func GetHomeDir(client *ssh.Client) string {
session, err := client.NewSession()
if err != nil {
return "~"
}
out, err := session.Output(`echo "$HOME"`)
if err == nil {
return strings.TrimSpace(string(out))
}
session, err = client.NewSession()
if err != nil {
return "~"
}
out, err = session.Output(`echo %userprofile%`)
if err == nil {
return strings.TrimSpace(string(out))
}
return "~"
}
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")
}