mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-04-17 20:39:47 +02:00
* working on re-init when you create a tab. some refactoring of existing reinit to make the messaging clearer. auto-connect, etc. * working to remove the 'default' shell states out of MShellProc. each tab should have its own state that gets set on open. * refactor newtab settings into individual components (and move to a new file) * more refactoring of tab settings -- use same control in settings and newtab * have screensettings use the same newtab settings components * use same conn dropdown, fix classes, update some of the confirm messages to be less confusing (replace screen with tab) * force a cr on a new tab to initialize state in a new line. poc right now, need to add to new workspace workflow as well * small fixups * remove nohist from GetRawStr, make const * update hover behavior for tabs * fix interaction between change remote dropdown, cmdinput, error handling, and selecting a remote * only switch screen remote if the activemainview is session (new remote flow). don't switch it if we're on the connections page which is confusing. also make it interactive * fix wording on tos modal * allow empty workspaces. also allow the last workspace to be deleted. (prep for new startup sequence where we initialize the first session after tos modal) * add some dead code that might come in use later (when we change how we show connection in cmdinput) * working a cople different angles. new settings tab-pulldown (likely orphaned). and then allowing null activeScreen and null activeSession in workspaceview (show appropriate messages, and give buttons to create new tabs/workspaces). prep for new startup flow * don't call initActiveShells anymore. also call ensureWorkspace() on TOS close * trying to use new pulldown screen settings * experiment with an escape keybinding * working on tab settings close triggers * close tab settings on tab switch * small updates to tos popup, reorder, update button text/size, small wording updates * when deleting a screen, send SIGHUP to all running commands * not sure how this happened, lineid should not be passed to setLineFocus * remove context timeouts for ReInit (it is now interactive, so it gets canceled like a normal command -- via ^C, and should not timeout on its own) * deal with screen/session tombstones updates (ignore to quite warning) * remove defaultfestate from remote * fix issue with removing default ris * remove dead code * open the settings pulldown for new screens * update prompt to show when the shell is still initializing (or if it failed) * switch buttons to use wave button class, update messages, and add warning for no shell state * all an override of rptr for dyncmds. needed for the 'connect' command (we need to set the rptr to the *new* connection rather than the old one) * remove old commented out code
306 lines
9.2 KiB
Go
306 lines
9.2 KiB
Go
// Copyright 2023, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package shellapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/alessio/shellescape"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/statediff"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
|
)
|
|
|
|
const BaseBashOpts = `set +m; set +H; shopt -s extglob`
|
|
|
|
const BashShellVersionCmdStr = `echo bash v${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}.${BASH_VERSINFO[2]}`
|
|
const RemoteBashPath = "bash"
|
|
|
|
// TODO fix bash path in these constants
|
|
const RunBashSudoCommandFmt = `sudo -n -C %d bash /dev/fd/%d`
|
|
const RunBashSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d bash -c "echo '[from-mshell]'; exec %d>&-; bash /dev/fd/%d < /dev/fd/%d"`
|
|
|
|
// do not use these directly, call GetLocalMajorVersion()
|
|
var localBashMajorVersionOnce = &sync.Once{}
|
|
var localBashMajorVersion = ""
|
|
|
|
// the "exec 2>" line also adds an extra printf at the *beginning* to strip out spurious rc file output
|
|
var GetBashShellStateCmds = []string{
|
|
"exec 2> /dev/null;",
|
|
BashShellVersionCmdStr + ";",
|
|
`pwd;`,
|
|
`declare -p $(compgen -A variable);`,
|
|
`alias -p;`,
|
|
`declare -f;`,
|
|
GetGitBranchCmdStr + ";",
|
|
}
|
|
|
|
type bashShellApi struct{}
|
|
|
|
func (b bashShellApi) GetShellType() string {
|
|
return packet.ShellType_bash
|
|
}
|
|
|
|
func (b bashShellApi) MakeExitTrap(fdNum int) (string, []byte) {
|
|
return MakeBashExitTrap(fdNum)
|
|
}
|
|
|
|
func (b bashShellApi) GetLocalMajorVersion() string {
|
|
return GetLocalBashMajorVersion()
|
|
}
|
|
|
|
func (b bashShellApi) GetLocalShellPath() string {
|
|
return GetLocalBashPath()
|
|
}
|
|
|
|
func (b bashShellApi) GetRemoteShellPath() string {
|
|
return RemoteBashPath
|
|
}
|
|
|
|
func (b bashShellApi) MakeRunCommand(cmdStr string, opts RunCommandOpts) string {
|
|
if !opts.Sudo {
|
|
return fmt.Sprintf(RunCommandFmt, cmdStr)
|
|
}
|
|
if opts.SudoWithPass {
|
|
return fmt.Sprintf(RunBashSudoPasswordCommandFmt, opts.PwFdNum, opts.MaxFdNum+1, opts.PwFdNum, opts.CommandFdNum, opts.CommandStdinFdNum)
|
|
} else {
|
|
return fmt.Sprintf(RunBashSudoCommandFmt, opts.MaxFdNum+1, opts.CommandFdNum)
|
|
}
|
|
}
|
|
|
|
func (b bashShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd {
|
|
return MakeBashShExecCommand(cmdStr, rcFileName, usePty)
|
|
}
|
|
|
|
func (b bashShellApi) GetShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
|
GetBashShellState(ctx, outCh, stdinDataCh)
|
|
}
|
|
|
|
func (b bashShellApi) GetBaseShellOpts() string {
|
|
return BaseBashOpts
|
|
}
|
|
|
|
func (b bashShellApi) ParseShellStateOutput(output []byte) (*packet.ShellState, *packet.ShellStateStats, error) {
|
|
return parseBashShellStateOutput(output)
|
|
}
|
|
|
|
func (b bashShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
|
var rcBuf bytes.Buffer
|
|
rcBuf.WriteString(b.GetBaseShellOpts() + "\n")
|
|
varDecls := shellenv.VarDeclsFromState(pk.State)
|
|
for _, varDecl := range varDecls {
|
|
if varDecl.IsExport() || varDecl.IsReadOnly() {
|
|
continue
|
|
}
|
|
if varDecl.IsExtVar {
|
|
continue
|
|
}
|
|
rcBuf.WriteString(BashDeclareStmt(varDecl))
|
|
rcBuf.WriteString("\n")
|
|
}
|
|
if pk.State != nil && pk.State.Funcs != "" {
|
|
rcBuf.WriteString(pk.State.Funcs)
|
|
rcBuf.WriteString("\n")
|
|
}
|
|
if pk.State != nil && pk.State.Aliases != "" {
|
|
rcBuf.WriteString(pk.State.Aliases)
|
|
rcBuf.WriteString("\n")
|
|
}
|
|
return rcBuf.String()
|
|
}
|
|
|
|
func GetBashShellStateCmd(fdNum int) (string, []byte) {
|
|
endBytes := utilfn.AppendNonZeroRandomBytes(nil, NumRandomEndBytes)
|
|
endBytes = append(endBytes, '\n')
|
|
cmdStr := strings.TrimSpace(`
|
|
exec 2> /dev/null;
|
|
exec > [%OUTPUTFD%];
|
|
printf "\x00\x00";
|
|
[%BASHVERSIONCMD%];
|
|
printf "\x00\x00";
|
|
pwd;
|
|
printf "\x00\x00";
|
|
declare -p $(compgen -A variable);
|
|
printf "\x00\x00";
|
|
alias -p;
|
|
printf "\x00\x00";
|
|
declare -f;
|
|
printf "\x00\x00";
|
|
[%GITBRANCHCMD%];
|
|
printf "\x00\x00";
|
|
printf "[%ENDBYTES%]";
|
|
`)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "[%OUTPUTFD%]", fmt.Sprintf("/dev/fd/%d", fdNum))
|
|
cmdStr = strings.ReplaceAll(cmdStr, "[%BASHVERSIONCMD%]", BashShellVersionCmdStr)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "[%GITBRANCHCMD%]", GetGitBranchCmdStr)
|
|
cmdStr = strings.ReplaceAll(cmdStr, "[%ENDBYTES%]", utilfn.ShellHexEscape(string(endBytes)))
|
|
return cmdStr, endBytes
|
|
}
|
|
|
|
func execGetLocalBashShellVersion() string {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), GetVersionTimeout)
|
|
defer cancelFn()
|
|
ecmd := exec.CommandContext(ctx, "bash", "-c", BashShellVersionCmdStr)
|
|
out, err := ecmd.Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
versionStr := strings.TrimSpace(string(out))
|
|
if strings.Index(versionStr, "bash ") == -1 {
|
|
// invalid shell version (only bash is supported)
|
|
return ""
|
|
}
|
|
return versionStr
|
|
}
|
|
|
|
func GetLocalBashMajorVersion() string {
|
|
localBashMajorVersionOnce.Do(func() {
|
|
fullVersion := execGetLocalBashShellVersion()
|
|
localBashMajorVersion = packet.GetMajorVersion(fullVersion)
|
|
})
|
|
return localBashMajorVersion
|
|
}
|
|
|
|
func GetBashShellState(ctx context.Context, outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
|
defer close(outCh)
|
|
stateCmd, endBytes := GetBashShellStateCmd(StateOutputFdNum)
|
|
cmdStr := BaseBashOpts + "; " + stateCmd
|
|
ecmd := exec.CommandContext(ctx, GetLocalBashPath(), "-l", "-i", "-c", cmdStr)
|
|
outputCh := make(chan []byte, 10)
|
|
var outputWg sync.WaitGroup
|
|
outputWg.Add(1)
|
|
go func() {
|
|
defer outputWg.Done()
|
|
for outputBytes := range outputCh {
|
|
outCh <- ShellStateOutput{Output: outputBytes}
|
|
}
|
|
}()
|
|
outputBytes, err := StreamCommandWithExtraFd(ctx, ecmd, outputCh, StateOutputFdNum, endBytes, stdinDataCh)
|
|
outputWg.Wait()
|
|
if err != nil {
|
|
outCh <- ShellStateOutput{Error: err.Error()}
|
|
return
|
|
}
|
|
rtn, stats, err := parseBashShellStateOutput(outputBytes)
|
|
if err != nil {
|
|
outCh <- ShellStateOutput{Error: err.Error()}
|
|
return
|
|
}
|
|
outCh <- ShellStateOutput{ShellState: rtn, Stats: stats}
|
|
}
|
|
|
|
func GetLocalBashPath() string {
|
|
if runtime.GOOS == "darwin" {
|
|
macShell := GetMacUserShell()
|
|
if strings.Index(macShell, "bash") != -1 {
|
|
return shellescape.Quote(macShell)
|
|
}
|
|
}
|
|
return "bash"
|
|
}
|
|
|
|
func GetLocalZshPath() string {
|
|
if runtime.GOOS == "darwin" {
|
|
macShell := GetMacUserShell()
|
|
if strings.Index(macShell, "zsh") != -1 {
|
|
return shellescape.Quote(macShell)
|
|
}
|
|
}
|
|
return "zsh"
|
|
}
|
|
|
|
func GetBashShellStateRedirectCommandStr(outputFdNum int) (string, []byte) {
|
|
cmdStr, endBytes := GetBashShellStateCmd(outputFdNum)
|
|
return cmdStr, endBytes
|
|
}
|
|
|
|
func MakeBashExitTrap(fdNum int) (string, []byte) {
|
|
stateCmd, endBytes := GetBashShellStateRedirectCommandStr(fdNum)
|
|
fmtStr := `
|
|
_waveshell_exittrap () {
|
|
%s
|
|
}
|
|
trap _waveshell_exittrap EXIT
|
|
`
|
|
return fmt.Sprintf(fmtStr, stateCmd), endBytes
|
|
}
|
|
|
|
func MakeBashShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd {
|
|
if usePty {
|
|
return exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-i", "-c", cmdStr)
|
|
} else {
|
|
return exec.Command(GetLocalBashPath(), "--rcfile", rcFileName, "-c", cmdStr)
|
|
}
|
|
}
|
|
|
|
func (bashShellApi) MakeShellStateDiff(oldState *packet.ShellState, oldStateHash string, newState *packet.ShellState) (*packet.ShellStateDiff, error) {
|
|
if oldState == nil {
|
|
return nil, fmt.Errorf("cannot diff, oldState is nil")
|
|
}
|
|
if newState == nil {
|
|
return nil, fmt.Errorf("cannot diff, newState is nil")
|
|
}
|
|
if !packet.StateVersionsCompatible(oldState.Version, newState.Version) {
|
|
return nil, fmt.Errorf("cannot diff, incompatible shell versions: %q %q", oldState.Version, newState.Version)
|
|
}
|
|
rtn := &packet.ShellStateDiff{}
|
|
rtn.BaseHash = oldStateHash
|
|
rtn.Version = newState.Version // always set version in the diff
|
|
if oldState.Cwd != newState.Cwd {
|
|
rtn.Cwd = newState.Cwd
|
|
}
|
|
rtn.Error = newState.Error
|
|
oldVars := shellenv.ShellStateVarsToMap(oldState.ShellVars)
|
|
newVars := shellenv.ShellStateVarsToMap(newState.ShellVars)
|
|
rtn.VarsDiff = statediff.MakeMapDiff(oldVars, newVars)
|
|
rtn.AliasesDiff = statediff.MakeLineDiff(oldState.Aliases, newState.Aliases, oldState.GetLineDiffSplitString())
|
|
rtn.FuncsDiff = statediff.MakeLineDiff(oldState.Funcs, newState.Funcs, oldState.GetLineDiffSplitString())
|
|
return rtn, nil
|
|
}
|
|
|
|
func (bashShellApi) ApplyShellStateDiff(oldState *packet.ShellState, diff *packet.ShellStateDiff) (*packet.ShellState, error) {
|
|
if oldState == nil {
|
|
return nil, fmt.Errorf("cannot apply diff, oldState is nil")
|
|
}
|
|
if diff == nil {
|
|
return oldState, nil
|
|
}
|
|
rtnState := &packet.ShellState{}
|
|
var err error
|
|
rtnState.Version = oldState.Version
|
|
// work around a bug (before v0.6.0) where version could be invalid.
|
|
// so only overwrite the oldversion if diff version is valid
|
|
_, _, diffVersionErr := packet.ParseShellStateVersion(diff.Version)
|
|
if diffVersionErr == nil {
|
|
rtnState.Version = diff.Version
|
|
}
|
|
rtnState.Cwd = oldState.Cwd
|
|
if diff.Cwd != "" {
|
|
rtnState.Cwd = diff.Cwd
|
|
}
|
|
rtnState.Error = diff.Error
|
|
oldVars := shellenv.ShellStateVarsToMap(oldState.ShellVars)
|
|
newVars, err := statediff.ApplyMapDiff(oldVars, diff.VarsDiff)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("applying mapdiff 'vars': %v", err)
|
|
}
|
|
rtnState.ShellVars = shellenv.StrMapToShellStateVars(newVars)
|
|
rtnState.Aliases, err = statediff.ApplyLineDiff(oldState.Aliases, diff.AliasesDiff)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("applying diff 'aliases': %v", err)
|
|
}
|
|
rtnState.Funcs, err = statediff.ApplyLineDiff(oldState.Funcs, diff.FuncsDiff)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("applying diff 'funcs': %v", err)
|
|
}
|
|
return rtnState, nil
|
|
}
|