2024-08-21 02:01:29 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
2024-05-15 01:53:03 +02:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package shellutil
|
|
|
|
|
|
|
|
import (
|
2024-05-15 07:37:04 +02:00
|
|
|
"context"
|
2024-08-01 08:47:33 +02:00
|
|
|
"fmt"
|
|
|
|
"log"
|
2024-05-15 01:53:03 +02:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2024-05-15 07:37:04 +02:00
|
|
|
"os/user"
|
2024-08-01 08:47:33 +02:00
|
|
|
"path/filepath"
|
2024-05-15 07:37:04 +02:00
|
|
|
"regexp"
|
|
|
|
"runtime"
|
2024-05-15 01:53:03 +02:00
|
|
|
"strings"
|
2024-05-15 07:37:04 +02:00
|
|
|
"sync"
|
|
|
|
"time"
|
2024-05-15 01:53:03 +02:00
|
|
|
|
2024-09-05 23:25:45 +02:00
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
2024-05-15 01:53:03 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const DefaultTermType = "xterm-256color"
|
|
|
|
const DefaultTermRows = 24
|
|
|
|
const DefaultTermCols = 80
|
|
|
|
|
2024-05-15 07:37:04 +02:00
|
|
|
var cachedMacUserShell string
|
|
|
|
var macUserShellOnce = &sync.Once{}
|
|
|
|
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
|
|
|
|
|
|
|
|
const DefaultShellPath = "/bin/bash"
|
|
|
|
|
2024-08-01 08:47:33 +02:00
|
|
|
const (
|
2024-09-05 23:05:42 +02:00
|
|
|
ZshIntegrationDir = "shell/zsh"
|
|
|
|
BashIntegrationDir = "shell/bash"
|
|
|
|
PwshIntegrationDir = "shell/pwsh"
|
2024-08-01 08:47:33 +02:00
|
|
|
WaveHomeBinDir = "bin"
|
|
|
|
|
|
|
|
ZshStartup_Zprofile = `
|
|
|
|
# Source the original zprofile
|
|
|
|
[ -f ~/.zprofile ] && source ~/.zprofile
|
|
|
|
`
|
|
|
|
|
|
|
|
ZshStartup_Zshrc = `
|
|
|
|
# Source the original zshrc
|
|
|
|
[ -f ~/.zshrc ] && source ~/.zshrc
|
|
|
|
|
2024-08-16 06:32:08 +02:00
|
|
|
export PATH={{.WSHBINDIR}}:$PATH
|
2024-09-11 20:14:38 +02:00
|
|
|
if [[ -n ${_comps+x} ]]; then
|
|
|
|
source <(wsh completion zsh)
|
|
|
|
fi
|
2024-08-01 08:47:33 +02:00
|
|
|
`
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2024-08-16 06:32:08 +02:00
|
|
|
export PATH={{.WSHBINDIR}}:$PATH
|
2024-09-11 20:14:38 +02:00
|
|
|
if type _init_completion &>/dev/null; then
|
|
|
|
source <(wsh completion bash)
|
|
|
|
fi
|
2024-09-10 07:26:10 +02:00
|
|
|
|
2024-09-04 11:13:00 +02:00
|
|
|
`
|
|
|
|
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
|
2024-08-01 08:47:33 +02:00
|
|
|
`
|
|
|
|
)
|
|
|
|
|
2024-05-15 07:37:04 +02:00
|
|
|
func DetectLocalShellPath() string {
|
2024-08-10 03:49:35 +02:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
return "powershell.exe"
|
|
|
|
}
|
2024-05-15 07:37:04 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-08-01 08:47:33 +02:00
|
|
|
// dscl . -read /Users/[username] UserShell
|
2024-05-15 07:37:04 +02:00
|
|
|
// 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]
|
|
|
|
}
|
|
|
|
|
2024-08-20 23:56:48 +02:00
|
|
|
func DefaultTermSize() waveobj.TermSize {
|
|
|
|
return waveobj.TermSize{Rows: DefaultTermRows, Cols: DefaultTermCols}
|
2024-08-19 06:26:44 +02:00
|
|
|
}
|
|
|
|
|
2024-08-01 08:47:33 +02:00
|
|
|
func WaveshellLocalEnvVars(termType string) map[string]string {
|
2024-05-15 01:53:03 +02:00
|
|
|
rtn := make(map[string]string)
|
|
|
|
if termType != "" {
|
|
|
|
rtn["TERM"] = termType
|
|
|
|
}
|
2024-08-01 08:47:33 +02:00
|
|
|
rtn["TERM_PROGRAM"] = "waveterm"
|
2024-05-15 01:53:03 +02:00
|
|
|
rtn["WAVETERM"], _ = os.Executable()
|
|
|
|
rtn["WAVETERM_VERSION"] = wavebase.WaveVersion
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
rtn["WAVETERM_WSHBINDIR"] = filepath.Join(wavebase.GetWaveDataDir(), WaveHomeBinDir)
|
2024-05-15 01:53:03 +02:00
|
|
|
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]
|
|
|
|
}
|
2024-08-01 08:47:33 +02:00
|
|
|
|
|
|
|
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 {
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
return filepath.Join(wavebase.GetWaveDataDir(), BashIntegrationDir, ".bashrc")
|
2024-08-01 08:47:33 +02:00
|
|
|
}
|
|
|
|
|
2024-09-04 11:13:00 +02:00
|
|
|
func GetWavePowershellEnv() string {
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
return filepath.Join(wavebase.GetWaveDataDir(), PwshIntegrationDir, "wavepwsh.ps1")
|
2024-09-04 11:13:00 +02:00
|
|
|
}
|
|
|
|
|
2024-08-01 08:47:33 +02:00
|
|
|
func GetZshZDotDir() string {
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
return filepath.Join(wavebase.GetWaveDataDir(), ZshIntegrationDir)
|
2024-08-01 08:47:33 +02:00
|
|
|
}
|
|
|
|
|
2024-09-04 20:23:39 +02:00
|
|
|
func GetWshBaseName(version string, goos string, goarch string) string {
|
2024-08-16 06:32:08 +02:00
|
|
|
ext := ""
|
2024-09-04 20:23:39 +02:00
|
|
|
if goarch == "amd64" {
|
|
|
|
goarch = "x64"
|
|
|
|
}
|
2024-09-06 22:19:38 +02:00
|
|
|
if goarch == "aarch64" {
|
|
|
|
goarch = "arm64"
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
if goos == "windows" {
|
|
|
|
ext = ".exe"
|
|
|
|
}
|
2024-09-04 20:23:39 +02:00
|
|
|
return fmt.Sprintf("wsh-%s-%s.%s%s", version, goos, goarch, ext)
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetWshBinaryPath(version string, goos string, goarch string) string {
|
2024-10-04 05:28:05 +02:00
|
|
|
return filepath.Join(wavebase.GetWaveAppBinPath(), GetWshBaseName(version, goos, goarch))
|
2024-08-16 06:32:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func InitRcFiles(waveHome string, wshBinDir string) error {
|
|
|
|
// ensure directiries exist
|
2024-08-01 08:47:33 +02:00
|
|
|
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
|
|
|
|
}
|
2024-09-04 11:13:00 +02:00
|
|
|
pwshDir := filepath.Join(waveHome, PwshIntegrationDir)
|
|
|
|
err = wavebase.CacheEnsureDir(pwshDir, PwshIntegrationDir, 0755, PwshIntegrationDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-08-01 08:47:33 +02:00
|
|
|
|
2024-08-16 06:32:08 +02:00
|
|
|
// write files to directory
|
2024-08-01 08:47:33 +02:00
|
|
|
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)
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(zshDir, ".zshrc"), ZshStartup_Zshrc, map[string]string{"WSHBINDIR": fmt.Sprintf(`"%s"`, wshBinDir)})
|
2024-08-01 08:47:33 +02:00
|
|
|
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)
|
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
err = utilfn.WriteTemplateToFile(filepath.Join(bashDir, ".bashrc"), BashStartup_Bashrc, map[string]string{"WSHBINDIR": fmt.Sprintf(`"%s"`, wshBinDir)})
|
2024-08-01 08:47:33 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error writing bash-integration .bashrc: %v", err)
|
|
|
|
}
|
2024-09-04 11:13:00 +02:00
|
|
|
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)
|
|
|
|
}
|
2024-08-01 08:47:33 +02:00
|
|
|
|
2024-08-16 06:32:08 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func initCustomShellStartupFilesInternal() error {
|
|
|
|
log.Printf("initializing wsh and shell startup files\n")
|
Update data and config paths to match platform defaults (#1047)
Going forward for new installations, config and data files will be
stored at the platform default paths, as defined by
[env-paths](https://www.npmjs.com/package/env-paths).
For backwards compatibility, if the `~/.waveterm` or `WAVETERM_HOME`
directory exists and contains valid data, it will be used. If this check
fails, then `WAVETERM_DATA_HOME` and `WAVETERM_CONFIG_HOME` will be
used. If these are not defined, then `XDG_DATA_HOME` and
`XDG_CONFIG_HOME` will be used. Finally, if none of these are defined,
the [env-paths](https://www.npmjs.com/package/env-paths) defaults will
be used.
As with the existing app, dev instances will write to `waveterm-dev`
directories, while all others will write to `waveterm`.
2024-10-22 18:26:58 +02:00
|
|
|
waveDataHome := wavebase.GetWaveDataDir()
|
|
|
|
binDir := filepath.Join(waveDataHome, WaveHomeBinDir)
|
|
|
|
err := InitRcFiles(waveDataHome, `$WAVETERM_WSHBINDIR`)
|
2024-08-16 06:32:08 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2024-08-01 08:47:33 +02:00
|
|
|
}
|
2024-08-16 06:32:08 +02:00
|
|
|
|
|
|
|
err = wavebase.CacheEnsureDir(binDir, WaveHomeBinDir, 0755, WaveHomeBinDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy the correct binary to bin
|
2024-09-04 20:23:39 +02:00
|
|
|
wshBaseName := GetWshBaseName(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH)
|
2024-08-16 06:32:08 +02:00
|
|
|
wshFullPath := GetWshBinaryPath(wavebase.WaveVersion, runtime.GOOS, runtime.GOARCH)
|
2024-08-01 08:47:33 +02:00
|
|
|
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")
|
2024-08-20 21:42:43 +02:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
wshDstPath = wshDstPath + ".exe"
|
|
|
|
}
|
2024-08-01 08:47:33 +02:00
|
|
|
err = utilfn.AtomicRenameCopy(wshDstPath, wshFullPath, 0755)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error copying wsh binary to bin: %v", err)
|
|
|
|
}
|
2024-10-04 05:28:05 +02:00
|
|
|
log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath)
|
2024-08-01 08:47:33 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-09-04 11:13:00 +02:00
|
|
|
func toPwshEnvVarRef(input string) string {
|
|
|
|
return strings.Replace(input, "$", "$env:", -1)
|
|
|
|
}
|