mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-06 19:18:22 +01:00
0781e6e821
* fix error logs in scws * new RpcFollowUpPacketType * make the rpc/followup handlers generic on the server side -- using new RpcHandlers map and RpcFollowUpPacketType * rpcinputpacket for passing user input back through to reinit command * add WAVETERM_DEV env var in dev mode * remove unused code, ensure mshell and rcfile directory on startup (prevent root clobber with sudo) * combine all feinput into one function msh.HandleFeInput, and add a new concept of input sinks for special cases (like reinit) * allow reset to accept user input (to get around interactive initialization problems) * tone down the selection background highlight color on dark mode. easier to read selected text * fix command focus and done focus issues with dynamic (non-run) commands * add 'module' as a 'rtnstate' command (#478) * reinitialize shells in parallel, fix timeouts, better error messages
1058 lines
32 KiB
Go
1058 lines
32 KiB
Go
// Copyright 2023, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package shellapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"github.com/alessio/shellescape"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/binpack"
|
|
"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"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
|
)
|
|
|
|
const BaseZshOpts = ``
|
|
|
|
const ZshShellVersionCmdStr = `echo zsh v$ZSH_VERSION`
|
|
|
|
const (
|
|
ZshSection_Version = iota
|
|
ZshSection_Cwd
|
|
ZshSection_Env
|
|
ZshSection_Mods
|
|
ZshSection_Vars
|
|
ZshSection_Aliases
|
|
ZshSection_Fpath
|
|
ZshSection_Funcs
|
|
ZshSection_PVars
|
|
ZshSection_Prompt
|
|
ZshSection_EndBytes
|
|
|
|
ZshSection_NumFieldsExpected // must be last
|
|
)
|
|
|
|
// TODO these need updating
|
|
const RunZshSudoCommandFmt = `sudo -n -C %d zsh /dev/fd/%d`
|
|
const RunZshSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d zsh -c "echo '[from-mshell]'; exec %d>&-; zsh /dev/fd/%d < /dev/fd/%d"`
|
|
|
|
var ZshIgnoreVars = map[string]bool{
|
|
"_": true,
|
|
"0": true,
|
|
"terminfo": true,
|
|
"RANDOM": true,
|
|
"COLUMNS": true,
|
|
"LINES": true,
|
|
"argv": true,
|
|
"SECONDS": true,
|
|
"PWD": true,
|
|
"HISTCHARS": true,
|
|
"HISTFILE": true,
|
|
"HISTSIZE": true,
|
|
"SAVEHIST": true,
|
|
"ZSH_EXECUTION_STRING": true,
|
|
"EPOCHSECONDS": true,
|
|
"EPOCHREALTIME": true,
|
|
"SHLVL": true,
|
|
"TTY": true,
|
|
"ZDOTDIR": true,
|
|
"PPID": true,
|
|
"epochtime": true,
|
|
"langinfo": true,
|
|
"keymaps": true,
|
|
"widgets": true,
|
|
"options": true,
|
|
"aliases": true,
|
|
"dis_aliases": true,
|
|
"saliases": true,
|
|
"dis_saliases": true,
|
|
"galiases": true,
|
|
"dis_galiases": true,
|
|
"builtins": true,
|
|
"dis_builtins": true,
|
|
"modules": true,
|
|
"history": true,
|
|
"historywords": true,
|
|
"jobdirs": true,
|
|
"jobstates": true,
|
|
"jobtexts": true,
|
|
"funcfiletrace": true,
|
|
"funcsourcetrace": true,
|
|
"funcstack": true,
|
|
"functrace": true,
|
|
"nameddirs": true,
|
|
"userdirs": true,
|
|
"parameters": true,
|
|
"commands": true,
|
|
"functions": true,
|
|
"dis_functions": true,
|
|
"functions_source": true,
|
|
"dis_functions_source": true,
|
|
"_comps": true,
|
|
"_patcomps": true,
|
|
"_postpatcomps": true,
|
|
|
|
// zsh/system
|
|
"errnos": true,
|
|
"sysparams": true,
|
|
|
|
// zsh/curses
|
|
"ZCURSES_COLORS": true,
|
|
"ZCURSES_COLOR_PAIRS": true,
|
|
"zcurses_attrs": true,
|
|
"zcurses_colors": true,
|
|
"zcurses_keycodes": true,
|
|
"zcurses_windows": true,
|
|
|
|
// not listed, but we also exclude all ZFTP_* variables
|
|
|
|
// powerlevel10k
|
|
"_GITSTATUS_CLIENT_PID_POWERLEVEL9K": true,
|
|
"GITSTATUS_DAEMON_PID_POWERLEVEL9K": true,
|
|
"_GITSTATUS_FILE_PREFIX_POWERLEVEL9K": true,
|
|
}
|
|
|
|
var ZshIgnoreFuncs = map[string]bool{
|
|
"zftp_chpwd": true,
|
|
"zftp_progress": true,
|
|
}
|
|
|
|
// only options we restore (other than ZshForceOptions)
|
|
var ZshIgnoreOptions = map[string]bool{
|
|
"globalrcs": true, // must stay off (otherwise /etc/zprofile runs)
|
|
"ksharrays": true,
|
|
"kshtypeset": true,
|
|
"kshautoload": true,
|
|
"kshzerosubscript": true,
|
|
"interactive": true,
|
|
"login": true,
|
|
"zle": true,
|
|
"shinstdin": true,
|
|
"privileged": true,
|
|
"restricted": true,
|
|
"singlecommand": true,
|
|
}
|
|
|
|
// force these options on/off at beginning of rcfile
|
|
var ZshForceOptions = map[string]bool{
|
|
"globalrcs": false,
|
|
"ksharrays": false,
|
|
"kshtypeset": false,
|
|
"kshautoload": false,
|
|
"kshzerosubscript": false,
|
|
"xtrace": false, // not in ZshIgnoreOptions
|
|
"verbose": false, // not in ZshIgnoreOptions
|
|
"debugbeforecmd": false, // not in ZshIgnoreOptions
|
|
}
|
|
|
|
var ZshUniqueArrayVars = map[string]bool{
|
|
"path": true,
|
|
"fpath": true,
|
|
}
|
|
|
|
var ZshSpecialDecls = map[string]bool{
|
|
"precmd_functions": true,
|
|
"preexec_functions": true,
|
|
}
|
|
|
|
var ZshUnsetVars = []string{
|
|
"HISTFILE",
|
|
"ZSH_EXECUTION_STRING",
|
|
}
|
|
|
|
var ZshForceLoadMods = map[string]bool{
|
|
"zsh/parameter": true,
|
|
"zsh/langinfo": true,
|
|
}
|
|
|
|
const ZModsVarName = "WAVESTATE_ZMODS"
|
|
|
|
// do not use these directly, call GetLocalMajorVersion()
|
|
var localZshMajorVersionOnce = &sync.Once{}
|
|
var localZshMajorVersion = ""
|
|
|
|
// sentinel value for functions that should be autoloaded
|
|
const ZshFnAutoLoad = "autoload"
|
|
const ZshAutoloadFnBody = "builtin autoload -XU"
|
|
|
|
type ZshParamKey struct {
|
|
// paramtype cannot contain spaces
|
|
// "aliases", "dis_aliases", "saliases", "dis_saliases", "galiases", "dis_galiases"
|
|
// "functions", "dis_functions", "functions_source", "dis_functions_source"
|
|
ParamType string
|
|
ParamName string
|
|
}
|
|
|
|
func (k ZshParamKey) String() string {
|
|
return k.ParamType + " " + k.ParamName
|
|
}
|
|
|
|
func ZshParamKeyFromString(s string) (ZshParamKey, error) {
|
|
parts := strings.SplitN(s, " ", 2)
|
|
if len(parts) != 2 {
|
|
return ZshParamKey{}, fmt.Errorf("invalid zsh param key")
|
|
}
|
|
return ZshParamKey{ParamType: parts[0], ParamName: parts[1]}, nil
|
|
}
|
|
|
|
type ZshMap = map[ZshParamKey]string
|
|
|
|
type zshShellApi struct{}
|
|
|
|
func (z zshShellApi) GetShellType() string {
|
|
return packet.ShellType_zsh
|
|
}
|
|
|
|
func (z zshShellApi) MakeExitTrap(fdNum int) (string, []byte) {
|
|
return MakeZshExitTrap(fdNum)
|
|
}
|
|
|
|
func (z zshShellApi) GetLocalMajorVersion() string {
|
|
return GetLocalZshMajorVersion()
|
|
}
|
|
|
|
func (z zshShellApi) GetLocalShellPath() string {
|
|
return "/bin/zsh"
|
|
}
|
|
|
|
func (z zshShellApi) GetRemoteShellPath() string {
|
|
return "zsh"
|
|
}
|
|
|
|
func (z zshShellApi) MakeRunCommand(cmdStr string, opts RunCommandOpts) string {
|
|
if !opts.Sudo {
|
|
return cmdStr
|
|
}
|
|
if opts.SudoWithPass {
|
|
return fmt.Sprintf(RunZshSudoPasswordCommandFmt, opts.PwFdNum, opts.MaxFdNum+1, opts.PwFdNum, opts.CommandFdNum, opts.CommandStdinFdNum)
|
|
} else {
|
|
return fmt.Sprintf(RunZshSudoCommandFmt, opts.MaxFdNum+1, opts.CommandFdNum)
|
|
}
|
|
}
|
|
|
|
func (z zshShellApi) MakeShExecCommand(cmdStr string, rcFileName string, usePty bool) *exec.Cmd {
|
|
return exec.Command(GetLocalZshPath(), "-l", "-i", "-c", cmdStr)
|
|
}
|
|
|
|
func (z zshShellApi) GetShellState(outCh chan ShellStateOutput, stdinDataCh chan []byte) {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
|
defer cancelFn()
|
|
defer close(outCh)
|
|
stateCmd, endBytes := GetZshShellStateCmd(StateOutputFdNum)
|
|
cmdStr := BaseZshOpts + "; " + stateCmd
|
|
ecmd := exec.CommandContext(ctx, GetLocalZshPath(), "-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 := z.ParseShellStateOutput(outputBytes)
|
|
if err != nil {
|
|
outCh <- ShellStateOutput{Error: err.Error()}
|
|
return
|
|
}
|
|
outCh <- ShellStateOutput{ShellState: rtn, Stats: stats}
|
|
}
|
|
|
|
func (z zshShellApi) GetBaseShellOpts() string {
|
|
return BaseZshOpts
|
|
}
|
|
|
|
func makeZshTypesetStmt(varDecl *shellenv.DeclareDeclType) string {
|
|
if !varDecl.IsZshDecl {
|
|
// not sure what to do here?
|
|
return ""
|
|
}
|
|
var argsStr string
|
|
if varDecl.Args == "" {
|
|
argsStr = "--"
|
|
} else {
|
|
argsStr = "-" + varDecl.Args
|
|
}
|
|
if varDecl.IsZshScalarBound() {
|
|
// varDecl.Value contains the extra "separator" field (if present in the original typeset def)
|
|
return fmt.Sprintf("typeset %s %s %s=%s", argsStr, varDecl.ZshBoundScalar, varDecl.Name, varDecl.Value)
|
|
} else {
|
|
return fmt.Sprintf("typeset %s %s=%s", argsStr, varDecl.Name, varDecl.Value)
|
|
}
|
|
}
|
|
|
|
func isZshSafeNameStr(name string) bool {
|
|
for _, ch := range name {
|
|
if ch == '_' {
|
|
continue
|
|
}
|
|
if unicode.IsLetter(ch) || unicode.IsDigit(ch) {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (z zshShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
|
var rcBuf bytes.Buffer
|
|
rcBuf.WriteString(z.GetBaseShellOpts() + "\n")
|
|
// rcBuf.WriteString("echo 'running generated rcfile' $0 $ZSH_ARGZERO '|' $ZDOTDIR\n")
|
|
varDecls := shellenv.VarDeclsFromState(pk.State)
|
|
// force options come at the beginning of the file (other options come at the end)
|
|
for optName, optVal := range ZshForceOptions {
|
|
if optVal {
|
|
rcBuf.WriteString(fmt.Sprintf("setopt %s\n", optName))
|
|
} else {
|
|
rcBuf.WriteString(fmt.Sprintf("unsetopt %s\n", optName))
|
|
}
|
|
}
|
|
for modName := range ZshForceLoadMods {
|
|
rcBuf.WriteString(fmt.Sprintf("zmodload %s\n", modName))
|
|
}
|
|
modDecl := getDeclByName(varDecls, ZModsVarName)
|
|
if modDecl != nil {
|
|
modsArr := utilfn.QuickParseJson[[]string](modDecl.Value)
|
|
for _, modName := range modsArr {
|
|
if !ZshForceLoadMods[modName] {
|
|
rcBuf.WriteString(fmt.Sprintf("zmodload %s\n", modName))
|
|
}
|
|
}
|
|
}
|
|
var postDecls []*shellenv.DeclareDeclType
|
|
for _, varDecl := range varDecls {
|
|
if ZshIgnoreVars[varDecl.Name] {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(varDecl.Name, "ZFTP_") {
|
|
continue
|
|
}
|
|
if varDecl.IsExtVar {
|
|
continue
|
|
}
|
|
if ZshUniqueArrayVars[varDecl.Name] && !varDecl.IsUniqueArray() {
|
|
varDecl.AddFlag("U")
|
|
}
|
|
if ZshSpecialDecls[varDecl.Name] {
|
|
postDecls = append(postDecls, varDecl)
|
|
continue
|
|
}
|
|
stmt := makeZshTypesetStmt(varDecl)
|
|
if stmt == "" {
|
|
continue
|
|
}
|
|
if varDecl.IsReadOnly() {
|
|
// we can't reset read-only variables
|
|
// so we check if it is a "safe" name, and then we can write a conditional
|
|
// that only sets it if it hasn't already been set.
|
|
if !isZshSafeNameStr(varDecl.Name) {
|
|
continue
|
|
}
|
|
rcBuf.WriteString(fmt.Sprintf("if (( ! ${+%s} )); then\n", varDecl.Name))
|
|
rcBuf.WriteString(makeZshTypesetStmt(varDecl))
|
|
rcBuf.WriteString("\nfi")
|
|
} else {
|
|
rcBuf.WriteString(makeZshTypesetStmt(varDecl))
|
|
}
|
|
rcBuf.WriteString("\n")
|
|
}
|
|
// do NOT unset ZDOTDIR, otherwise initialization will start to read initialization files from ~/ again
|
|
for _, varName := range ZshUnsetVars {
|
|
rcBuf.WriteString("unset " + shellescape.Quote(varName) + "\n")
|
|
}
|
|
|
|
// aliases
|
|
aliasMap, err := DecodeZshMap([]byte(pk.State.Aliases))
|
|
if err != nil {
|
|
base.Logf("error decoding zsh aliases: %v\n", err)
|
|
rcBuf.WriteString("# error decoding zsh aliases\n")
|
|
} else {
|
|
for aliasKey, aliasValue := range aliasMap {
|
|
// tricky here, don't quote AliasName (it gets implicit quotes, and quoting doesn't work as expected)
|
|
aliasStr := fmt.Sprintf("%s[%s]=%s\n", aliasKey.ParamType, aliasKey.ParamName, shellescape.Quote(aliasValue))
|
|
rcBuf.WriteString(aliasStr)
|
|
}
|
|
}
|
|
|
|
// functions
|
|
fnMap, err := DecodeZshMap([]byte(pk.State.Funcs))
|
|
if err != nil {
|
|
base.Logf("error decoding zsh functions: %v\n", err)
|
|
rcBuf.WriteString("# error decoding zsh functions\n")
|
|
} else {
|
|
for fnKey, fnValue := range fnMap {
|
|
if ZshIgnoreFuncs[fnKey.ParamName] {
|
|
continue
|
|
}
|
|
if fnValue == ZshFnAutoLoad {
|
|
rcBuf.WriteString(fmt.Sprintf("autoload %s\n", shellescape.Quote(fnKey.ParamName)))
|
|
} else {
|
|
// careful, no whitespace (except newlines)
|
|
rcBuf.WriteString(fmt.Sprintf("function %s () {\n%s\n}\n", shellescape.Quote(fnKey.ParamName), fnValue))
|
|
if fnKey.ParamType == "dis_functions" {
|
|
rcBuf.WriteString(fmt.Sprintf("disable -f %s\n", shellescape.Quote(fnKey.ParamName)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// write postdecls
|
|
for _, varDecl := range postDecls {
|
|
rcBuf.WriteString(makeZshTypesetStmt(varDecl))
|
|
rcBuf.WriteString("\n")
|
|
}
|
|
writeZshOptions(&rcBuf, varDecls)
|
|
return rcBuf.String()
|
|
}
|
|
|
|
func writeZshOptions(rcBuf *bytes.Buffer, declArr []*shellenv.DeclareDeclType) {
|
|
optionDecl := getDeclByName(declArr, "options")
|
|
var optionsMap map[string]string
|
|
if optionDecl != nil {
|
|
var err error
|
|
optionsMap, err = parseSimpleZshOptions(optionDecl.Value)
|
|
if err != nil {
|
|
wlog.Logf("error decoding zsh options: %v\n", err)
|
|
}
|
|
}
|
|
for optName := range optionsMap {
|
|
if ZshIgnoreOptions[optName] {
|
|
continue
|
|
}
|
|
if optionsMap[optName] == "on" {
|
|
rcBuf.WriteString(fmt.Sprintf("setopt %s\n", optName))
|
|
} else {
|
|
rcBuf.WriteString(fmt.Sprintf("unsetopt %s\n", optName))
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeZshId(buf *bytes.Buffer, idStr string) {
|
|
buf.WriteString(shellescape.Quote(idStr))
|
|
}
|
|
|
|
const numRandomBytes = 4
|
|
|
|
// returns (cmd-string, endbytes)
|
|
func GetZshShellStateCmd(fdNum int) (string, []byte) {
|
|
var sectionSeparator []byte
|
|
// adding this extra "\n" helps with debuging and readability of output
|
|
sectionSeparator = append(sectionSeparator, byte('\n'))
|
|
sectionSeparator = utilfn.AppendNonZeroRandomBytes(sectionSeparator, numRandomBytes)
|
|
sectionSeparator = append(sectionSeparator, 0, 0)
|
|
endBytes := utilfn.AppendNonZeroRandomBytes(nil, NumRandomEndBytes)
|
|
endBytes = append(endBytes, byte('\n'))
|
|
// we have to use these crazy separators because zsh allows basically anything in
|
|
// variable names and values (including nulls).
|
|
// note that we don't need crazy separators for "env" or "typeset".
|
|
// environment variables *cannot* contain nulls by definition, and "typeset" already escapes nulls.
|
|
// the raw aliases and functions though need to be handled more carefully
|
|
// output redirection is necessary to prevent cooked tty options from screwing up the output (funcs especially)
|
|
// note we do not need the "extra" separator that bashapi uses because we are reading from OUTPUTFD (which already excludes any spurious stdout/stderr data)
|
|
cmd := `
|
|
exec > [%OUTPUTFD%]
|
|
unsetopt SH_WORD_SPLIT;
|
|
zmodload zsh/parameter;
|
|
zmodload zsh/langinfo;
|
|
[%ZSHVERSION%];
|
|
printf "\x00[%SECTIONSEP%]";
|
|
pwd;
|
|
printf "[%SECTIONSEP%]";
|
|
env -0;
|
|
printf "[%SECTIONSEP%]";
|
|
zmodload -L
|
|
printf "[%SECTIONSEP%]";
|
|
typeset -p +H -m '*';
|
|
printf "[%SECTIONSEP%]";
|
|
for var in "${(@k)aliases}"; do
|
|
printf "aliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${aliases[$var]}
|
|
done
|
|
for var in "${(@k)dis_aliases}"; do
|
|
printf "dis_aliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_aliases[$var]}
|
|
done
|
|
for var in "${(@k)saliases}"; do
|
|
printf "saliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${saliases[$var]}
|
|
done
|
|
for var in "${(@k)dis_saliases}"; do
|
|
printf "dis_saliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_saliases[$var]}
|
|
done
|
|
for var in "${(@k)galiases}"; do
|
|
printf "galiases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${galiases[$var]}
|
|
done
|
|
for var in "${(@k)dis_galiases}"; do
|
|
printf "dis_galiases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_galiases[$var]}
|
|
done
|
|
printf "[%SECTIONSEP%]";
|
|
echo $FPATH;
|
|
printf "[%SECTIONSEP%]";
|
|
for var in "${(@k)functions}"; do
|
|
printf "functions %s[%PARTSEP%]%s[%PARTSEP%]" $var ${functions[$var]}
|
|
done
|
|
for var in "${(@k)dis_functions}"; do
|
|
printf "dis_functions %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_functions[$var]}
|
|
done
|
|
for var in "${(@k)functions_source}"; do
|
|
printf "functions_source %s[%PARTSEP%]%s[%PARTSEP%]" $var ${functions_source[$var]}
|
|
done
|
|
for var in "${(@k)dis_functions_source}"; do
|
|
printf "dis_functions_source %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_functions_source[$var]}
|
|
done
|
|
printf "[%SECTIONSEP%]";
|
|
[%GITBRANCH%]
|
|
[%K8SCONTEXT%]
|
|
[%K8SNAMESPACE%]
|
|
printf "[%SECTIONSEP%]";
|
|
print -P "$PS1"
|
|
printf "[%SECTIONSEP%]";
|
|
printf "[%ENDBYTES%]"
|
|
`
|
|
cmd = strings.TrimSpace(cmd)
|
|
cmd = strings.ReplaceAll(cmd, "[%ZSHVERSION%]", ZshShellVersionCmdStr)
|
|
cmd = strings.ReplaceAll(cmd, "[%GITBRANCH%]", GetGitBranchCmdStr)
|
|
cmd = strings.ReplaceAll(cmd, "[%K8SCONTEXT%]", GetK8sContextCmdStr)
|
|
cmd = strings.ReplaceAll(cmd, "[%K8SNAMESPACE%]", GetK8sNamespaceCmdStr)
|
|
cmd = strings.ReplaceAll(cmd, "[%PARTSEP%]", utilfn.ShellHexEscape(string(sectionSeparator[0:len(sectionSeparator)-1])))
|
|
cmd = strings.ReplaceAll(cmd, "[%SECTIONSEP%]", utilfn.ShellHexEscape(string(sectionSeparator)))
|
|
cmd = strings.ReplaceAll(cmd, "[%OUTPUTFD%]", fmt.Sprintf("/dev/fd/%d", fdNum))
|
|
cmd = strings.ReplaceAll(cmd, "[%OUTPUTFDNUM%]", fmt.Sprintf("%d", fdNum))
|
|
cmd = strings.ReplaceAll(cmd, "[%ENDBYTES%]", utilfn.ShellHexEscape(string(endBytes)))
|
|
return cmd, endBytes
|
|
}
|
|
|
|
func MakeZshExitTrap(fdNum int) (string, []byte) {
|
|
stateCmd, endBytes := GetZshShellStateCmd(fdNum)
|
|
fmtStr := `
|
|
zshexit () {
|
|
%s
|
|
}
|
|
`
|
|
return fmt.Sprintf(fmtStr, stateCmd), endBytes
|
|
}
|
|
|
|
func execGetLocalZshShellVersion() string {
|
|
ctx, cancelFn := context.WithTimeout(context.Background(), GetStateTimeout)
|
|
defer cancelFn()
|
|
ecmd := exec.CommandContext(ctx, "zsh", "-c", ZshShellVersionCmdStr)
|
|
out, err := ecmd.Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
versionStr := strings.TrimSpace(string(out))
|
|
if strings.Index(versionStr, "zsh ") == -1 {
|
|
return ""
|
|
}
|
|
return versionStr
|
|
}
|
|
|
|
func GetLocalZshMajorVersion() string {
|
|
localZshMajorVersionOnce.Do(func() {
|
|
fullVersion := execGetLocalZshShellVersion()
|
|
localZshMajorVersion = packet.GetMajorVersion(fullVersion)
|
|
})
|
|
return localZshMajorVersion
|
|
}
|
|
|
|
func EncodeZshMap(m ZshMap) []byte {
|
|
var buf bytes.Buffer
|
|
binpack.PackUInt(&buf, uint64(len(m)))
|
|
orderedKeys := utilfn.GetOrderedStringerMapKeys(m)
|
|
for _, key := range orderedKeys {
|
|
value := m[key]
|
|
binpack.PackValue(&buf, []byte(key.String()))
|
|
binpack.PackValue(&buf, []byte(value))
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func EncodeZshMapForApply(m map[string][]byte) string {
|
|
var buf bytes.Buffer
|
|
binpack.PackUInt(&buf, uint64(len(m)))
|
|
orderedKeys := utilfn.GetOrderedMapKeys(m)
|
|
for _, key := range orderedKeys {
|
|
value := m[key]
|
|
binpack.PackValue(&buf, []byte(key))
|
|
binpack.PackValue(&buf, value)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func DecodeZshMapForDiff(barr []byte) (map[string][]byte, error) {
|
|
rtn := make(map[string][]byte)
|
|
buf := bytes.NewBuffer(barr)
|
|
u := binpack.MakeUnpacker(buf)
|
|
numEntries := u.UnpackUInt("numEntries")
|
|
for idx := 0; idx < numEntries; idx++ {
|
|
key := string(u.UnpackValue("key"))
|
|
value := u.UnpackValue("value")
|
|
rtn[key] = value
|
|
}
|
|
if u.Error() != nil {
|
|
return nil, u.Error()
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func DecodeZshMap(barr []byte) (ZshMap, error) {
|
|
rtn := make(ZshMap)
|
|
buf := bytes.NewBuffer(barr)
|
|
u := binpack.MakeUnpacker(buf)
|
|
numEntries := u.UnpackUInt("numEntries")
|
|
for idx := 0; idx < numEntries; idx++ {
|
|
key := string(u.UnpackValue("key"))
|
|
value := string(u.UnpackValue("value"))
|
|
zshKey, err := ZshParamKeyFromString(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtn[zshKey] = value
|
|
}
|
|
if u.Error() != nil {
|
|
return nil, u.Error()
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func parseZshAliasStateOutput(aliasBytes []byte, partSeparator []byte) map[ZshParamKey]string {
|
|
aliasParts := bytes.Split(aliasBytes, partSeparator)
|
|
rtn := make(map[ZshParamKey]string)
|
|
for aliasPartIdx := 0; aliasPartIdx < len(aliasParts)-1; aliasPartIdx += 2 {
|
|
aliasNameAndType := string(aliasParts[aliasPartIdx])
|
|
aliasNameAndTypeParts := strings.SplitN(aliasNameAndType, " ", 2)
|
|
if len(aliasNameAndTypeParts) != 2 {
|
|
continue
|
|
}
|
|
aliasKey := ZshParamKey{ParamType: aliasNameAndTypeParts[0], ParamName: aliasNameAndTypeParts[1]}
|
|
aliasValue := string(aliasParts[aliasPartIdx+1])
|
|
rtn[aliasKey] = aliasValue
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func isSourceFileInFpath(fpathArr []string, sourceFile string) bool {
|
|
for _, fpath := range fpathArr {
|
|
if fpath == "" || fpath == "." {
|
|
continue
|
|
}
|
|
firstChar := fpath[0]
|
|
if firstChar != '/' && firstChar != '~' {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(sourceFile, fpath) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ParseZshFunctions(fpathArr []string, fnBytes []byte, partSeparator []byte) map[ZshParamKey]string {
|
|
fnBody := make(map[ZshParamKey]string)
|
|
fnSource := make(map[string]string)
|
|
fnParts := bytes.Split(fnBytes, partSeparator)
|
|
for fnPartIdx := 0; fnPartIdx < len(fnParts)-1; fnPartIdx += 2 {
|
|
fnTypeAndName := string(fnParts[fnPartIdx])
|
|
fnValue := string(fnParts[fnPartIdx+1])
|
|
fnTypeAndNameParts := strings.SplitN(fnTypeAndName, " ", 2)
|
|
if len(fnTypeAndNameParts) != 2 {
|
|
continue
|
|
}
|
|
fnType := fnTypeAndNameParts[0]
|
|
fnName := fnTypeAndNameParts[1]
|
|
if fnName == "zshexit" {
|
|
continue
|
|
}
|
|
if ZshIgnoreFuncs[fnName] {
|
|
continue
|
|
}
|
|
if fnType == "functions" || fnType == "dis_functions" {
|
|
fnBody[ZshParamKey{ParamType: fnType, ParamName: fnName}] = fnValue
|
|
}
|
|
if fnType == "functions_source" || fnType == "dis_functions_source" {
|
|
fnSource[fnName] = fnValue
|
|
}
|
|
}
|
|
// ok, so the trick here is that we want to only include functions that are *not* autoloaded
|
|
// the ones that are pending autoloading or come from a source file in fpath, can just be set to autoload
|
|
for fnKey := range fnBody {
|
|
var inFpath bool
|
|
source := fnSource[fnKey.ParamName]
|
|
if source != "" {
|
|
inFpath = isSourceFileInFpath(fpathArr, source)
|
|
}
|
|
isAutoloadFnBody := strings.TrimSpace(fnBody[fnKey]) == ZshAutoloadFnBody
|
|
if inFpath || isAutoloadFnBody {
|
|
fnBody[fnKey] = ZshFnAutoLoad
|
|
}
|
|
}
|
|
return fnBody
|
|
}
|
|
|
|
func makeZshFuncsStrForShellState(fnMap map[ZshParamKey]string) string {
|
|
var buf bytes.Buffer
|
|
for fnKey, fnValue := range fnMap {
|
|
buf.WriteString(fmt.Sprintf("%s %s %s\x00", fnKey.ParamType, fnKey.ParamName, fnValue))
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (z zshShellApi) ParseShellStateOutput(outputBytes []byte) (*packet.ShellState, *packet.ShellStateStats, error) {
|
|
if scbase.IsDevMode() && DebugState {
|
|
writeStateToFile(packet.ShellType_zsh, outputBytes)
|
|
}
|
|
firstZeroIdx := bytes.Index(outputBytes, []byte{0})
|
|
firstDZeroIdx := bytes.Index(outputBytes, []byte{0, 0})
|
|
if firstZeroIdx == -1 || firstDZeroIdx == -1 {
|
|
return nil, nil, fmt.Errorf("invalid zsh shell state output, could not parse separator bytes")
|
|
}
|
|
versionStr := string(outputBytes[0:firstZeroIdx])
|
|
sectionSeparator := outputBytes[firstZeroIdx+1 : firstDZeroIdx+2]
|
|
partSeparator := sectionSeparator[0 : len(sectionSeparator)-1]
|
|
// sections: see ZshSection_* consts
|
|
sections := bytes.Split(outputBytes, sectionSeparator)
|
|
if len(sections) != ZshSection_NumFieldsExpected {
|
|
return nil, nil, fmt.Errorf("invalid zsh shell state output, wrong number of sections, section=%d", len(sections))
|
|
}
|
|
rtn := &packet.ShellState{}
|
|
rtn.Version = strings.TrimSpace(versionStr)
|
|
if rtn.GetShellType() != packet.ShellType_zsh {
|
|
return nil, nil, fmt.Errorf("invalid zsh shell state output, wrong shell type")
|
|
}
|
|
if _, _, err := packet.ParseShellStateVersion(rtn.Version); err != nil {
|
|
return nil, nil, fmt.Errorf("invalid zsh shell state output, invalid version: %v", err)
|
|
}
|
|
cwdStr := stripNewLineChars(string(sections[ZshSection_Cwd]))
|
|
rtn.Cwd = cwdStr
|
|
zshEnv := parseZshEnv(sections[ZshSection_Env])
|
|
zshDecls, err := parseZshDecls(sections[ZshSection_Vars])
|
|
if err != nil {
|
|
base.Logf("invalid - parsedecls %v\n", err)
|
|
return nil, nil, err
|
|
}
|
|
for _, decl := range zshDecls {
|
|
if decl.IsZshScalarBound() {
|
|
decl.ZshEnvValue = zshEnv[decl.ZshBoundScalar]
|
|
}
|
|
}
|
|
aliasMap := parseZshAliasStateOutput(sections[ZshSection_Aliases], partSeparator)
|
|
rtn.Aliases = string(EncodeZshMap(aliasMap))
|
|
fpathStr := stripNewLineChars(string(string(sections[ZshSection_Fpath])))
|
|
fpathArr := strings.Split(fpathStr, ":")
|
|
zshFuncs := ParseZshFunctions(fpathArr, sections[ZshSection_Funcs], partSeparator)
|
|
rtn.Funcs = string(EncodeZshMap(zshFuncs))
|
|
pvarMap := parseExtVarOutput(sections[ZshSection_PVars], string(sections[ZshSection_Prompt]), string(sections[ZshSection_Mods]))
|
|
utilfn.CombineMaps(zshDecls, pvarMap)
|
|
rtn.ShellVars = shellenv.SerializeDeclMap(zshDecls)
|
|
stats := &packet.ShellStateStats{
|
|
Version: rtn.Version,
|
|
AliasCount: int(len(aliasMap)),
|
|
FuncCount: int(len(zshFuncs)),
|
|
VarCount: int(len(zshDecls)),
|
|
EnvCount: int(len(zshEnv)),
|
|
HashVal: rtn.GetHashVal(false),
|
|
OutputSize: int64(len(outputBytes)),
|
|
StateSize: rtn.ApproximateSize(),
|
|
}
|
|
return rtn, stats, nil
|
|
}
|
|
|
|
func parseZshEnv(output []byte) map[string]string {
|
|
outputStr := string(output)
|
|
lines := strings.Split(outputStr, "\x00")
|
|
rtn := make(map[string]string)
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
eqIdx := strings.Index(line, "=")
|
|
if eqIdx == -1 {
|
|
continue
|
|
}
|
|
name := line[0:eqIdx]
|
|
if ZshIgnoreVars[name] {
|
|
continue
|
|
}
|
|
val := line[eqIdx+1:]
|
|
rtn[name] = val
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func parseZshScalarBoundAssignment(declStr string, decl *DeclareDeclType) error {
|
|
declStr = strings.TrimLeft(declStr, " ")
|
|
spaceIdx := strings.Index(declStr, " ")
|
|
if spaceIdx == -1 {
|
|
return fmt.Errorf("invalid zsh decl (scalar bound): %q", declStr)
|
|
}
|
|
decl.ZshBoundScalar = declStr[0:spaceIdx]
|
|
standardDecl := declStr[spaceIdx+1:]
|
|
return parseStandardZshAssignment(standardDecl, decl)
|
|
}
|
|
|
|
func parseStandardZshAssignment(declStr string, decl *DeclareDeclType) error {
|
|
declStr = strings.TrimLeft(declStr, " ")
|
|
eqIdx := strings.Index(declStr, "=")
|
|
if eqIdx == -1 {
|
|
return fmt.Errorf("invalid zsh decl: %q", declStr)
|
|
}
|
|
decl.Name = declStr[0:eqIdx]
|
|
decl.Value = declStr[eqIdx+1:]
|
|
return nil
|
|
}
|
|
|
|
func parseZshDeclAssignment(declStr string, decl *DeclareDeclType) error {
|
|
if decl.IsZshScalarBound() {
|
|
return parseZshScalarBoundAssignment(declStr, decl)
|
|
}
|
|
return parseStandardZshAssignment(declStr, decl)
|
|
}
|
|
|
|
// returns (newDeclStr, argsStr, err)
|
|
func parseZshDeclArgs(declStr string, isExport bool) (string, string, error) {
|
|
origDeclStr := declStr
|
|
var argsStr string
|
|
if isExport {
|
|
argsStr = "x"
|
|
}
|
|
declStr = strings.TrimLeft(declStr, " ")
|
|
for strings.HasPrefix(declStr, "-") {
|
|
spaceIdx := strings.Index(declStr, " ")
|
|
if spaceIdx == -1 {
|
|
return "", "", fmt.Errorf("invalid zsh export line: %q", origDeclStr)
|
|
}
|
|
newArgsStr := strings.TrimSpace(declStr[1:spaceIdx])
|
|
argsStr = argsStr + newArgsStr
|
|
declStr = declStr[spaceIdx+1:]
|
|
declStr = strings.TrimLeft(declStr, " ")
|
|
}
|
|
return declStr, argsStr, nil
|
|
}
|
|
|
|
func stripNewLineChars(s string) string {
|
|
for {
|
|
if len(s) == 0 {
|
|
return s
|
|
}
|
|
lastChar := s[len(s)-1]
|
|
if lastChar == '\n' || lastChar == '\r' {
|
|
s = s[0 : len(s)-1]
|
|
} else {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseZshDeclLine(line string) (*DeclareDeclType, error) {
|
|
line = stripNewLineChars(line)
|
|
if strings.HasPrefix(line, "export ") {
|
|
exportLine := line[7:]
|
|
assignLine, exportArgs, err := parseZshDeclArgs(exportLine, true)
|
|
rtn := &DeclareDeclType{IsZshDecl: true, Args: exportArgs}
|
|
err = parseZshDeclAssignment(assignLine, rtn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rtn, nil
|
|
} else if strings.HasPrefix(line, "typeset ") {
|
|
typesetLine := line[8:]
|
|
assignLine, typesetArgs, err := parseZshDeclArgs(typesetLine, false)
|
|
rtn := &DeclareDeclType{IsZshDecl: true, Args: typesetArgs}
|
|
err = parseZshDeclAssignment(assignLine, rtn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rtn, nil
|
|
} else {
|
|
return nil, fmt.Errorf("invalid zsh decl line: %q", line)
|
|
}
|
|
}
|
|
|
|
// combine decl2 INTO decl1
|
|
func combineTiedZshDecls(decl1 *DeclareDeclType, decl2 *DeclareDeclType) {
|
|
if decl2.IsExport() {
|
|
decl1.AddFlag("x")
|
|
}
|
|
if decl2.IsArray() {
|
|
decl1.AddFlag("a")
|
|
}
|
|
}
|
|
|
|
func parseZshDecls(output []byte) (map[string]*DeclareDeclType, error) {
|
|
// NOTES:
|
|
// - we get extra \r characters in the output (trimmed in parseZshDeclLine) (we get \r\n)
|
|
// - tied variables (-T) are printed twice! this is especially confusing for exported vars:
|
|
// (1) `export -T PATH path=( ... )`
|
|
// (2) `typeset -aT PATH path=( ... )`
|
|
// we have to "combine" these two lines into one decl.
|
|
outputStr := string(output)
|
|
lines := strings.Split(outputStr, "\n")
|
|
rtn := make(map[string]*DeclareDeclType)
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
decl, err := parseZshDeclLine(line)
|
|
if err != nil {
|
|
base.Logf("error parsing zsh decl line: %v", err)
|
|
continue
|
|
}
|
|
if decl == nil {
|
|
continue
|
|
}
|
|
if ZshIgnoreVars[decl.Name] {
|
|
continue
|
|
}
|
|
if rtn[decl.Name] != nil && decl.IsZshScalarBound() {
|
|
combineTiedZshDecls(rtn[decl.Name], decl)
|
|
continue
|
|
}
|
|
rtn[decl.Name] = decl
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func makeZshMapDiff(oldMap string, newMap string) ([]byte, error) {
|
|
oldMapMap, err := DecodeZshMapForDiff([]byte(oldMap))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error zshMapDiff decoding old-zsh map: %v", err)
|
|
}
|
|
newMapMap, err := DecodeZshMapForDiff([]byte(newMap))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error zshMapDiff decoding new-zsh map: %v", err)
|
|
}
|
|
return statediff.MakeMapDiff(oldMapMap, newMapMap), nil
|
|
}
|
|
|
|
func applyZshMapDiff(oldMap string, diff []byte) (string, error) {
|
|
oldMapMap, err := DecodeZshMapForDiff([]byte(oldMap))
|
|
if err != nil {
|
|
return "", fmt.Errorf("error zshMapDiff decoding old-zsh map: %v", err)
|
|
}
|
|
newMapMap, err := statediff.ApplyMapDiff(oldMapMap, diff)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error zshMapDiff applying diff: %v", err)
|
|
}
|
|
return EncodeZshMapForApply(newMapMap), nil
|
|
}
|
|
|
|
func (zshShellApi) 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 oldState.Version != newState.Version {
|
|
return nil, fmt.Errorf("cannot diff, states have different versions")
|
|
}
|
|
rtn := &packet.ShellStateDiff{}
|
|
rtn.BaseHash = oldStateHash
|
|
rtn.Version = newState.Version // always set version
|
|
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)
|
|
var err error
|
|
rtn.AliasesDiff, err = makeZshMapDiff(oldState.Aliases, newState.Aliases)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtn.FuncsDiff, err = makeZshMapDiff(oldState.Funcs, newState.Funcs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func (zshShellApi) 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
|
|
if diff.Version != rtnState.Version {
|
|
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 = applyZshMapDiff(oldState.Aliases, diff.AliasesDiff)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("applying diff 'aliases': %v", err)
|
|
}
|
|
rtnState.Funcs, err = applyZshMapDiff(oldState.Funcs, diff.FuncsDiff)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("applying diff 'funcs': %v", err)
|
|
}
|
|
return rtnState, nil
|
|
}
|
|
|
|
// this will *not* parse general zsh assoc arrays, used to parse zsh options (no spaces)
|
|
// ( [posixargzero]=off [autolist]=on )
|
|
func parseSimpleZshOptions(decl string) (map[string]string, error) {
|
|
decl = strings.TrimSpace(decl)
|
|
if !strings.HasPrefix(decl, "(") || !strings.HasSuffix(decl, ")") {
|
|
return nil, errors.New("invalid assoc array decl, must start and end with parens")
|
|
}
|
|
decl = decl[1 : len(decl)-1]
|
|
parts := strings.Split(decl, " ")
|
|
rtn := make(map[string]string)
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
eqIdx := strings.Index(part, "=")
|
|
if eqIdx == -1 {
|
|
return nil, fmt.Errorf("invalid assoc array decl part: %q", part)
|
|
}
|
|
bracketedKey := part[0:eqIdx]
|
|
val := part[eqIdx+1:]
|
|
if !strings.HasPrefix(bracketedKey, "[") || !strings.HasSuffix(bracketedKey, "]") {
|
|
return nil, fmt.Errorf("invalid assoc array decl part: %q", part)
|
|
}
|
|
key := bracketedKey[1 : len(bracketedKey)-1]
|
|
rtn[key] = val
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func getDeclByName(decls []*shellenv.DeclareDeclType, name string) *shellenv.DeclareDeclType {
|
|
for _, decl := range decls {
|
|
if decl.Name == name {
|
|
return decl
|
|
}
|
|
}
|
|
return nil
|
|
}
|