zsh fixes (fix for #368), clean up zsh option handling, more debugging code (#387)

* logging and add a flag to debug returnstate file

* fix for issue #368.  clean up more zsh options.  force global_rcs to be off.
This commit is contained in:
Mike Sawka 2024-03-06 11:38:27 -08:00 committed by GitHub
parent 7bf1a259a2
commit a1ab25936e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 8 deletions

View File

@ -37,7 +37,8 @@ const LogFileName = "mshell.log"
const ForceDebugLog = false
const DebugFlag_LogRcFile = "logrc"
const LogRcFileName = "debug.rcfile"
const DebugRcFileName = "debug.rcfile"
const DebugReturnStateFileName = "debug.returnstate"
const (
ProcessType_Unknown = "unknown"
@ -175,7 +176,12 @@ func HasDebugFlag(envMap map[string]string, flagName string) bool {
func GetDebugRcFileName() string {
msHome := GetMShellHomeDir()
return path.Join(msHome, LogRcFileName)
return path.Join(msHome, DebugRcFileName)
}
func GetDebugReturnStateFileName() string {
msHome := GetMShellHomeDir()
return path.Join(msHome, DebugReturnStateFileName)
}
func GetHomeDir() string {

View File

@ -6,6 +6,7 @@ package shellapi
import (
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"os/exec"
@ -20,6 +21,7 @@ import (
"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"
)
@ -56,7 +58,7 @@ var ZshIgnoreVars = map[string]bool{
"langinfo": true,
"keymaps": true,
"widgets": true,
"options": true,
"aliases": true,
"dis_aliases": true,
"saliases": true,
@ -86,6 +88,34 @@ var ZshIgnoreVars = map[string]bool{
"_postpatcomps": 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,
@ -101,6 +131,11 @@ var ZshUnsetVars = []string{
"ZSH_EXECUTION_STRING",
}
var ZshLoadMods = []string{
"zsh/parameter",
"zsh/langinfo",
}
// do not use these directly, call GetLocalMajorVersion()
var localZshMajorVersionOnce = &sync.Once{}
var localZshMajorVersion = ""
@ -223,10 +258,19 @@ func isZshSafeNameStr(name string) bool {
func (z zshShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
var rcBuf bytes.Buffer
rcBuf.WriteString(z.GetBaseShellOpts() + "\n")
rcBuf.WriteString("unsetopt GLOBAL_RCS\n")
rcBuf.WriteString("unset KSH_ARRAYS\n")
rcBuf.WriteString("zmodload zsh/parameter\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 ZshLoadMods {
rcBuf.WriteString(fmt.Sprintf("zmodload %s\n", modName))
}
var postDecls []*shellenv.DeclareDeclType
for _, varDecl := range varDecls {
if ZshIgnoreVars[varDecl.Name] {
@ -294,16 +338,37 @@ func (z zshShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
}
}
}
// 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))
}
@ -334,6 +399,7 @@ func GetZshShellStateCmd(fdNum int) string {
exec > [%OUTPUTFD%]
unsetopt SH_WORD_SPLIT;
zmodload zsh/parameter;
zmodload zsh/langinfo;
[%ZSHVERSION%];
printf "\x00[%SECTIONSEP%]";
pwd;
@ -853,3 +919,42 @@ func (zshShellApi) ApplyShellStateDiff(oldState *packet.ShellState, diff *packet
}
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
}

View File

@ -31,6 +31,7 @@ import (
"github.com/wavetermdev/waveterm/waveshell/pkg/shellapi"
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
"golang.org/x/mod/semver"
"golang.org/x/sys/unix"
)
@ -51,6 +52,7 @@ const SigKillWaitTime = 2 * time.Second
const RtnStateFdNum = 20
const ReturnStateReadWaitTime = 2 * time.Second
const ForceDebugRcFile = false
const ForceDebugReturnState = false
const ClientCommandFmt = `
PATH=$PATH:~/.mshell;
@ -833,6 +835,7 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
}
shellVarMap := shellenv.ShellVarMapFromState(state)
if base.HasDebugFlag(shellVarMap, base.DebugFlag_LogRcFile) || ForceDebugRcFile {
wlog.Logf("debugrc file %q\n", base.GetDebugRcFileName())
debugRcFileName := base.GetDebugRcFileName()
err := os.WriteFile(debugRcFileName, []byte(rcFileStr), 0600)
if err != nil {
@ -1196,6 +1199,10 @@ func (c *ShExecType) WaitForCommand() *packet.CmdDonePacketType {
c.ReturnState.Reader.Close()
}()
<-c.ReturnState.DoneCh
if ForceDebugReturnState {
wlog.Logf("debug returnstate file %q\n", base.GetDebugReturnStateFileName())
os.WriteFile(base.GetDebugReturnStateFileName(), c.ReturnState.Buf, 0666)
}
state, _ := c.SAPI.ParseShellStateOutput(c.ReturnState.Buf) // TODO what to do with error?
donePacket.FinalState = state
}