mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-04 18:59:08 +01:00
4357bcf1c8
* checkpoint some ideas on a new branch * checkpoint on new errors / errorcode passing * get CodedError piped all the way through to infomsg * implement a /reset:cwd command to deal with cases when the cwd is invalid. other assorted debugging, utility, and fixups * on invalid cwd, show message to run /reset:cwd
269 lines
7.0 KiB
Go
269 lines
7.0 KiB
Go
// Copyright 2023, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package shellapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/alessio/shellescape"
|
|
"github.com/creack/pty"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
|
)
|
|
|
|
const GetStateTimeout = 5 * time.Second
|
|
const GetGitBranchCmdStr = `printf "GITBRANCH %s\x00" "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"`
|
|
const RunCommandFmt = `%s`
|
|
const DebugState = false
|
|
|
|
var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
|
|
|
|
var cachedMacUserShell string
|
|
var macUserShellOnce = &sync.Once{}
|
|
|
|
const DefaultMacOSShell = "/bin/bash"
|
|
|
|
type RunCommandOpts struct {
|
|
Sudo bool
|
|
SudoWithPass bool
|
|
MaxFdNum int // needed for Sudo
|
|
CommandFdNum int // needed for Sudo
|
|
PwFdNum int // needed for SudoWithPass
|
|
CommandStdinFdNum int // needed for SudoWithPass
|
|
}
|
|
|
|
const (
|
|
ShellStateOutputStatus_Done = "done"
|
|
)
|
|
|
|
type ShellStateOutput struct {
|
|
Status string
|
|
StderrOutput []byte
|
|
ShellState *packet.ShellState
|
|
Error string
|
|
}
|
|
|
|
type ShellApi interface {
|
|
GetShellType() string
|
|
MakeExitTrap(fdNum int) string
|
|
GetLocalMajorVersion() string
|
|
GetLocalShellPath() string
|
|
GetRemoteShellPath() string
|
|
MakeRunCommand(cmdStr string, opts RunCommandOpts) string
|
|
MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd
|
|
GetShellState() chan ShellStateOutput
|
|
GetBaseShellOpts() string
|
|
ParseShellStateOutput(output []byte) (*packet.ShellState, error)
|
|
MakeRcFileStr(pk *packet.RunPacketType) string
|
|
MakeShellStateDiff(oldState *packet.ShellState, oldStateHash string, newState *packet.ShellState) (*packet.ShellStateDiff, error)
|
|
ApplyShellStateDiff(oldState *packet.ShellState, diff *packet.ShellStateDiff) (*packet.ShellState, error)
|
|
}
|
|
|
|
var _ ShellApi = &bashShellApi{}
|
|
var _ ShellApi = &zshShellApi{}
|
|
|
|
func DetectLocalShellType() string {
|
|
shellPath := GetMacUserShell()
|
|
if shellPath == "" {
|
|
shellPath = os.Getenv("SHELL")
|
|
}
|
|
if shellPath == "" {
|
|
return packet.ShellType_bash
|
|
}
|
|
_, file := filepath.Split(shellPath)
|
|
if strings.HasPrefix(file, "zsh") {
|
|
return packet.ShellType_zsh
|
|
}
|
|
return packet.ShellType_bash
|
|
}
|
|
|
|
func HasShell(shellType string) bool {
|
|
if shellType == packet.ShellType_bash {
|
|
_, err := exec.LookPath("bash")
|
|
return err != nil
|
|
}
|
|
if shellType == packet.ShellType_zsh {
|
|
_, err := exec.LookPath("zsh")
|
|
return err != nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
func MakeShellApi(shellType string) (ShellApi, error) {
|
|
if shellType == "" || shellType == packet.ShellType_bash {
|
|
return &bashShellApi{}, nil
|
|
}
|
|
if shellType == packet.ShellType_zsh {
|
|
return &zshShellApi{}, nil
|
|
}
|
|
return nil, fmt.Errorf("shell type not supported: %s", shellType)
|
|
}
|
|
|
|
func GetMacUserShell() string {
|
|
if runtime.GOOS != "darwin" {
|
|
return ""
|
|
}
|
|
macUserShellOnce.Do(func() {
|
|
cachedMacUserShell = internalMacUserShell()
|
|
})
|
|
return cachedMacUserShell
|
|
}
|
|
|
|
// dscl . -read /User/[username] UserShell
|
|
// defaults to /bin/bash
|
|
func internalMacUserShell() string {
|
|
osUser, err := user.Current()
|
|
if err != nil {
|
|
return DefaultMacOSShell
|
|
}
|
|
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 DefaultMacOSShell
|
|
}
|
|
outStr := strings.TrimSpace(string(out))
|
|
m := userShellRegexp.FindStringSubmatch(outStr)
|
|
if m == nil {
|
|
return DefaultMacOSShell
|
|
}
|
|
return m[1]
|
|
}
|
|
|
|
const FirstExtraFilesFdNum = 3
|
|
|
|
// returns output(stdout+stderr), extraFdOutput, error
|
|
func RunCommandWithExtraFd(ecmd *exec.Cmd, extraFdNum int) ([]byte, []byte, error) {
|
|
ecmd.Env = os.Environ()
|
|
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType))
|
|
cmdPty, cmdTty, err := pty.Open()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("opening new pty: %w", err)
|
|
}
|
|
defer cmdTty.Close()
|
|
defer cmdPty.Close()
|
|
pty.Setsize(cmdPty, &pty.Winsize{Rows: shellutil.DefaultTermRows, Cols: shellutil.DefaultTermCols})
|
|
ecmd.Stdin = cmdTty
|
|
ecmd.Stdout = cmdTty
|
|
ecmd.Stderr = cmdTty
|
|
ecmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
ecmd.SysProcAttr.Setsid = true
|
|
ecmd.SysProcAttr.Setctty = true
|
|
pipeReader, pipeWriter, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not create pipe: %w", err)
|
|
}
|
|
defer pipeWriter.Close()
|
|
defer pipeReader.Close()
|
|
extraFiles := make([]*os.File, extraFdNum+1)
|
|
extraFiles[extraFdNum] = pipeWriter
|
|
ecmd.ExtraFiles = extraFiles[FirstExtraFilesFdNum:]
|
|
defer pipeReader.Close()
|
|
ecmd.Start()
|
|
cmdTty.Close()
|
|
pipeWriter.Close()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var outputWg sync.WaitGroup
|
|
var outputBuf bytes.Buffer
|
|
var extraFdOutputBuf bytes.Buffer
|
|
outputWg.Add(2)
|
|
go func() {
|
|
// ignore error (/dev/ptmx has read error when process is done)
|
|
defer outputWg.Done()
|
|
io.Copy(&outputBuf, cmdPty)
|
|
}()
|
|
go func() {
|
|
defer outputWg.Done()
|
|
io.Copy(&extraFdOutputBuf, pipeReader)
|
|
}()
|
|
exitErr := ecmd.Wait()
|
|
if exitErr != nil {
|
|
return nil, nil, exitErr
|
|
}
|
|
outputWg.Wait()
|
|
return outputBuf.Bytes(), extraFdOutputBuf.Bytes(), nil
|
|
}
|
|
|
|
func RunSimpleCmdInPty(ecmd *exec.Cmd) ([]byte, error) {
|
|
ecmd.Env = os.Environ()
|
|
shellutil.UpdateCmdEnv(ecmd, shellutil.MShellEnvVars(shellutil.DefaultTermType))
|
|
cmdPty, cmdTty, err := pty.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening new pty: %w", err)
|
|
}
|
|
pty.Setsize(cmdPty, &pty.Winsize{Rows: shellutil.DefaultTermRows, Cols: shellutil.DefaultTermCols})
|
|
ecmd.Stdin = cmdTty
|
|
ecmd.Stdout = cmdTty
|
|
ecmd.Stderr = cmdTty
|
|
ecmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
ecmd.SysProcAttr.Setsid = true
|
|
ecmd.SysProcAttr.Setctty = true
|
|
err = ecmd.Start()
|
|
cmdTty.Close()
|
|
if err != nil {
|
|
cmdPty.Close()
|
|
return nil, err
|
|
}
|
|
defer cmdPty.Close()
|
|
ioDone := make(chan bool)
|
|
var outputBuf bytes.Buffer
|
|
go func() {
|
|
// ignore error (/dev/ptmx has read error when process is done)
|
|
io.Copy(&outputBuf, cmdPty)
|
|
close(ioDone)
|
|
}()
|
|
exitErr := ecmd.Wait()
|
|
if exitErr != nil {
|
|
return nil, exitErr
|
|
}
|
|
<-ioDone
|
|
return outputBuf.Bytes(), nil
|
|
}
|
|
|
|
func parsePVarOutput(pvarBytes []byte, isZsh bool) map[string]*DeclareDeclType {
|
|
declMap := make(map[string]*DeclareDeclType)
|
|
pvars := bytes.Split(pvarBytes, []byte{0})
|
|
for _, pvarBA := range pvars {
|
|
pvarStr := string(pvarBA)
|
|
pvarFields := strings.SplitN(pvarStr, " ", 2)
|
|
if len(pvarFields) != 2 {
|
|
continue
|
|
}
|
|
if pvarFields[0] == "" {
|
|
continue
|
|
}
|
|
decl := &DeclareDeclType{IsZshDecl: isZsh, Args: "x"}
|
|
decl.Name = "PROMPTVAR_" + pvarFields[0]
|
|
decl.Value = shellescape.Quote(pvarFields[1])
|
|
declMap[decl.Name] = decl
|
|
}
|
|
return declMap
|
|
}
|
|
|
|
// for debugging (not for production use)
|
|
func writeStateToFile(shellType string, outputBytes []byte) error {
|
|
msHome := base.GetMShellHomeDir()
|
|
stateFileName := path.Join(msHome, shellType+"-state.txt")
|
|
os.WriteFile(stateFileName, outputBytes, 0644)
|
|
return nil
|
|
}
|