diff --git a/waveshell/pkg/base/base.go b/waveshell/pkg/base/base.go index 82f04236..bdd98a5b 100644 --- a/waveshell/pkg/base/base.go +++ b/waveshell/pkg/base/base.go @@ -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 { diff --git a/waveshell/pkg/shellapi/zshapi.go b/waveshell/pkg/shellapi/zshapi.go index f2222657..4b53dc8c 100644 --- a/waveshell/pkg/shellapi/zshapi.go +++ b/waveshell/pkg/shellapi/zshapi.go @@ -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 +} diff --git a/waveshell/pkg/shexec/shexec.go b/waveshell/pkg/shexec/shexec.go index 2264440b..abb0effa 100644 --- a/waveshell/pkg/shexec/shexec.go +++ b/waveshell/pkg/shexec/shexec.go @@ -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 }