waveterm/pkg/util/shellutil/shellutil.go
Evan Simkowitz 74cda378f8
Embed static copy of docsite for help view (#949)
This will take the latest artifact from the waveterm-docs repo and embed
it in the app binary. When the help view is launched, it will be served
from our backend. If the embedded copy doesn't exist, such as in
unpackaged versions of the app or in locally packaged versions, it will
use the hosted site instead.

There is a sibling PR in the docs repository to build the embedded
version of the app (strips out some external links, removes Algolia
DocSearch, updates the baseUrl)
https://github.com/wavetermdev/waveterm-docs/pull/46
2024-10-03 20:28:05 -07:00

326 lines
8.5 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package shellutil
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
)
const DefaultTermType = "xterm-256color"
const DefaultTermRows = 24
const DefaultTermCols = 80
var cachedMacUserShell string
var macUserShellOnce = &sync.Once{}
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
const DefaultShellPath = "/bin/bash"
const (
ZshIntegrationDir = "shell/zsh"
BashIntegrationDir = "shell/bash"
PwshIntegrationDir = "shell/pwsh"
WaveHomeBinDir = "bin"
ZshStartup_Zprofile = `
# Source the original zprofile
[ -f ~/.zprofile ] && source ~/.zprofile
`
ZshStartup_Zshrc = `
# Source the original zshrc
[ -f ~/.zshrc ] && source ~/.zshrc
export PATH={{.WSHBINDIR}}:$PATH
if [[ -n ${_comps+x} ]]; then
source <(wsh completion zsh)
fi
`
ZshStartup_Zlogin = `
# Source the original zlogin
[ -f ~/.zlogin ] && source ~/.zlogin
`
ZshStartup_Zshenv = `
[ -f ~/.zshenv ] && source ~/.zshenv
`
BashStartup_Bashrc = `
# Source /etc/profile if it exists
if [ -f /etc/profile ]; then
. /etc/profile
fi
# Source the first of ~/.bash_profile, ~/.bash_login, or ~/.profile that exists
if [ -f ~/.bash_profile ]; then
. ~/.bash_profile
elif [ -f ~/.bash_login ]; then
. ~/.bash_login
elif [ -f ~/.profile ]; then
. ~/.profile
fi
export PATH={{.WSHBINDIR}}:$PATH
if type _init_completion &>/dev/null; then
source <(wsh completion bash)
fi
`
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
`
)
func DetectLocalShellPath() string {
if runtime.GOOS == "windows" {
return "powershell.exe"
}
shellPath := GetMacUserShell()
if shellPath == "" {
shellPath = os.Getenv("SHELL")
}
if shellPath == "" {
return DefaultShellPath
}
return shellPath
}
func GetMacUserShell() string {
if runtime.GOOS != "darwin" {
return ""
}
macUserShellOnce.Do(func() {
cachedMacUserShell = internalMacUserShell()
})
return cachedMacUserShell
}
// dscl . -read /Users/[username] UserShell
// defaults to /bin/bash
func internalMacUserShell() string {
osUser, err := user.Current()
if err != nil {
return DefaultShellPath
}
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
userStr := "/Users/" + osUser.Username
out, err := exec.CommandContext(ctx, "dscl", ".", "-read", userStr, "UserShell").CombinedOutput()
if err != nil {
return DefaultShellPath
}
outStr := strings.TrimSpace(string(out))
m := userShellRegexp.FindStringSubmatch(outStr)
if m == nil {
return DefaultShellPath
}
return m[1]
}
func DefaultTermSize() waveobj.TermSize {
return waveobj.TermSize{Rows: DefaultTermRows, Cols: DefaultTermCols}
}
func WaveshellLocalEnvVars(termType string) map[string]string {
rtn := make(map[string]string)
if termType != "" {
rtn["TERM"] = termType
}
rtn["TERM_PROGRAM"] = "waveterm"
rtn["WAVETERM"], _ = os.Executable()
rtn["WAVETERM_VERSION"] = wavebase.WaveVersion
rtn["WAVETERM_WSHBINDIR"] = filepath.Join(wavebase.GetWaveHomeDir(), WaveHomeBinDir)
return rtn
}
func UpdateCmdEnv(cmd *exec.Cmd, envVars map[string]string) {
if len(envVars) == 0 {
return
}
found := make(map[string]bool)
var newEnv []string
for _, envStr := range cmd.Env {
envKey := GetEnvStrKey(envStr)
newEnvVal, ok := envVars[envKey]
if ok {
if newEnvVal == "" {
continue
}
newEnv = append(newEnv, envKey+"="+newEnvVal)
found[envKey] = true
} else {
newEnv = append(newEnv, envStr)
}
}
for envKey, envVal := range envVars {
if found[envKey] {
continue
}
newEnv = append(newEnv, envKey+"="+envVal)
}
cmd.Env = newEnv
}
func GetEnvStrKey(envStr string) string {
eqIdx := strings.Index(envStr, "=")
if eqIdx == -1 {
return envStr
}
return envStr[0:eqIdx]
}
var initStartupFilesOnce = &sync.Once{}
// in a Once block so it can be called multiple times
// we run it at startup, but also before launching local shells so we know everything is initialized before starting the shell
func InitCustomShellStartupFiles() error {
var err error
initStartupFilesOnce.Do(func() {
err = initCustomShellStartupFilesInternal()
})
return err
}
func GetBashRcFileOverride() string {
return filepath.Join(wavebase.GetWaveHomeDir(), BashIntegrationDir, ".bashrc")
}
func GetWavePowershellEnv() string {
return filepath.Join(wavebase.GetWaveHomeDir(), PwshIntegrationDir, "wavepwsh.ps1")
}
func GetZshZDotDir() string {
return filepath.Join(wavebase.GetWaveHomeDir(), ZshIntegrationDir)
}
func GetWshBaseName(version string, goos string, goarch string) string {
ext := ""
if goarch == "amd64" {
goarch = "x64"
}
if goarch == "aarch64" {
goarch = "arm64"
}
if goos == "windows" {
ext = ".exe"
}
return fmt.Sprintf("wsh-%s-%s.%s%s", version, goos, goarch, ext)
}
func GetWshBinaryPath(version string, goos string, goarch string) string {
return filepath.Join(wavebase.GetWaveAppBinPath(), GetWshBaseName(version, goos, goarch))
}
func InitRcFiles(waveHome string, wshBinDir string) error {
// ensure directiries exist
zshDir := filepath.Join(waveHome, ZshIntegrationDir)
err := wavebase.CacheEnsureDir(zshDir, ZshIntegrationDir, 0755, ZshIntegrationDir)
if err != nil {
return err
}
bashDir := filepath.Join(waveHome, BashIntegrationDir)
err = wavebase.CacheEnsureDir(bashDir, BashIntegrationDir, 0755, BashIntegrationDir)
if err != nil {
return err
}
pwshDir := filepath.Join(waveHome, PwshIntegrationDir)
err = wavebase.CacheEnsureDir(pwshDir, PwshIntegrationDir, 0755, PwshIntegrationDir)
if err != nil {
return err
}
// write files to directory
zprofilePath := filepath.Join(zshDir, ".zprofile")
err = os.WriteFile(zprofilePath, []byte(ZshStartup_Zprofile), 0644)
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)})
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)
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)
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)})
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(pwshDir, "wavepwsh.ps1"), PwshStartup_wavepwsh, map[string]string{"WSHBINDIR": toPwshEnvVarRef(wshBinDir), "PATHSEP": pathSep})
if err != nil {
return fmt.Errorf("error writing pwsh-integration wavepwsh.ps1: %v", err)
}
return nil
}
func initCustomShellStartupFilesInternal() error {
log.Printf("initializing wsh and shell startup files\n")
waveHome := wavebase.GetWaveHomeDir()
binDir := filepath.Join(waveHome, WaveHomeBinDir)
err := InitRcFiles(waveHome, `$WAVETERM_WSHBINDIR`)
if err != nil {
return err
}
err = wavebase.CacheEnsureDir(binDir, WaveHomeBinDir, 0755, WaveHomeBinDir)
if err != nil {
return err
}
// copy the correct binary to bin
wshBaseName := GetWshBaseName(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH)
wshFullPath := GetWshBinaryPath(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH)
if _, err := os.Stat(wshFullPath); err != nil {
log.Printf("error (non-fatal), could not resolve wsh binary %q: %v\n", wshFullPath, err)
return nil
}
wshDstPath := filepath.Join(binDir, "wsh")
if runtime.GOOS == "windows" {
wshDstPath = wshDstPath + ".exe"
}
err = utilfn.AtomicRenameCopy(wshDstPath, wshFullPath, 0755)
if err != nil {
return fmt.Errorf("error copying wsh binary to bin: %v", err)
}
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)
}