diff --git a/go.mod b/go.mod index fcecba2..302512c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/pingcap/ticat go 1.16 -require github.com/mattn/go-shellwords v1.0.11 +require ( + github.com/mattn/go-shellwords v1.0.11 + github.com/peterh/liner v1.2.1 // indirect +) diff --git a/go.sum b/go.sum index 6042a20..cca446d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw= github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= +github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index d0c8dc2..9e3b390 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -710,6 +710,9 @@ func RegisterDbgCmds(cmds *core.CmdTree) { AddArg("seconds", "3", "second", "sec", "s", "S") breaks := cmds.AddSub("breaks", "break") + breaks.RegPowerCmd(DbgBreakAtBegin, + "set break point at the first command"). + SetQuiet() breaksAt := breaks.AddSub("at", "before"). RegPowerCmd(DbgBreakBefore, @@ -723,6 +726,11 @@ func RegisterDbgCmds(cmds *core.CmdTree) { "set break point at the first command"). SetQuiet() + breaksAt.AddSub("end", "finish"). + RegPowerCmd(DbgBreakAtEnd, + "set break point after the last command"). + SetQuiet() + breaks.AddSub("after", "post"). RegPowerCmd(DbgBreakAfter, // TODO: get 'sep' from env or other config @@ -753,8 +761,12 @@ func RegisterDbgCmds(cmds *core.CmdTree) { "verify bash in os/exec"). SetQuiet() - cmds.AddSub("interact", "i", "I"). - AddSub("leave", "l", "L"). + interact := cmds.AddSub("interact", "interactive", "i", "I"). + RegPowerCmd(DbgInteract, + "enter interact mode"). + SetQuiet() + + interact.AddSub("leave", "l", "L"). RegPowerCmd(DbgInteractLeave, "leave interact mode and continue to run") diff --git a/pkg/builtin/dbg.go b/pkg/builtin/dbg.go index 6d47857..77fe967 100644 --- a/pkg/builtin/dbg.go +++ b/pkg/builtin/dbg.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/pingcap/ticat/pkg/cli/core" - //"github.com/pingcap/ticat/pkg/cli/display" ) func DbgEcho( @@ -88,6 +87,21 @@ func DbgBreakAtBegin( return currCmdIdx, true } +func DbgBreakAtEnd( + argv core.ArgVals, + cc *core.Cli, + env *core.Env, + flow *core.ParsedCmds, + currCmdIdx int) (int, bool) { + + assertNotTailMode(flow, currCmdIdx) + cc.BreakPoints.SetAtEnd(true) + if 1 == len(flow.Cmds)-1 { + env.GetLayer(core.EnvLayerSession).SetBool("display.one-cmd", true) + } + return currCmdIdx, true +} + func DbgBreakBefore( argv core.ArgVals, cc *core.Cli, @@ -131,3 +145,16 @@ func DbgInteractLeave( env.SetBool("sys.interact.leaving", true) return currCmdIdx, true } + +func DbgInteract( + argv core.ArgVals, + cc *core.Cli, + env *core.Env, + flow *core.ParsedCmds, + currCmdIdx int) (int, bool) { + + assertNotTailMode(flow, currCmdIdx) + + InteractiveMode(cc, env, "e") + return currCmdIdx, true +} diff --git a/pkg/builtin/interactive.go b/pkg/builtin/interactive.go new file mode 100644 index 0000000..17cbf88 --- /dev/null +++ b/pkg/builtin/interactive.go @@ -0,0 +1,92 @@ +package builtin + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/peterh/liner" + + "github.com/pingcap/ticat/pkg/cli/core" + "github.com/pingcap/ticat/pkg/cli/display" +) + +func InteractiveMode(cc *core.Cli, env *core.Env, exitStr string) { + cc.Screen.Print(display.ColorExplain("(ctl-c to leave)\n", env)) + + cc = cc.CopyForInteract() + sessionEnv := env.GetLayer(core.EnvLayerSession) + sessionEnv.SetBool("sys.interact.inside", true) + + seqSep := env.GetRaw("strs.seq-sep") + selfName := env.GetRaw("strs.self-name") + + lineReader := liner.NewLiner() + defer lineReader.Close() + lineReader.SetCtrlCAborts(true) + + names := cc.Cmds.GatherNames() + lineReader.SetCompleter(func(line string) (res []string) { + fields := strings.Fields(line) + field := strings.TrimLeft(fields[len(fields)-1], seqSep) + prefix := line[0 : len(line)-len(field)] + for _, name := range names { + if strings.HasPrefix(name, field) { + res = append(res, prefix+name) + } + } + return + }) + + historyDir := filepath.Join(os.TempDir(), ".ticat_interact_mode_cmds_history") + if file, err := os.Open(historyDir); err == nil { + lineReader.ReadHistory(file) + file.Close() + } + + for { + if env.GetBool("sys.interact.leaving") { + break + } + line, err := lineReader.Prompt(selfName + "> ") + if err == liner.ErrPromptAborted { + break + } + if err != nil { + panic(fmt.Errorf("[InteractMode] read from stdin failed: %v", err)) + } + + lineReader.AppendHistory(line) + executorSafeExecute("(interact)", cc, env, nil, core.FlowStrToStrs(line)...) + } + + sessionEnv.GetLayer(core.EnvLayerSession).Delete("sys.interact.inside") + + file, err := os.Create(historyDir) + if err != nil { + panic(fmt.Errorf("[InteractMode] error writing history file: %v", err)) + } + lineReader.WriteHistory(file) + file.Close() +} + +func executorSafeExecute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) { + env = env.GetLayer(core.EnvLayerSession) + stackDepth := env.GetRaw("sys.stack-depth") + stack := env.GetRaw("sys.stack") + + defer func() { + env.Set("sys.stack-depth", stackDepth) + env.Set("sys.stack", stack) + + if !env.GetBool("sys.panic.recover") { + return + } + if r := recover(); r != nil { + display.PrintError(cc, env, r.(error)) + } + }() + + cc.Executor.Execute(caller, false, cc, env, masks, input...) +} diff --git a/pkg/cli/core/breakpoints.go b/pkg/cli/core/breakpoints.go index 359e1a1..25141cb 100644 --- a/pkg/cli/core/breakpoints.go +++ b/pkg/cli/core/breakpoints.go @@ -5,17 +5,30 @@ import ( ) type BreakPoints struct { - Begin bool + AtBegin bool + AtEnd bool Befores map[string]bool Afters map[string]bool } func NewBreakPoints() *BreakPoints { - return &BreakPoints{false, map[string]bool{}, map[string]bool{}} + return &BreakPoints{false, false, map[string]bool{}, map[string]bool{}} } func (self *BreakPoints) SetAtBegin(enabled bool) { - self.Begin = enabled + self.AtBegin = enabled +} + +func (self *BreakPoints) BreakAtBegin() bool { + return self.AtBegin +} + +func (self *BreakPoints) SetAtEnd(enabled bool) { + self.AtEnd = enabled +} + +func (self *BreakPoints) BreakAtEnd() bool { + return self.AtEnd } func (self *BreakPoints) SetBefores(cc *Cli, env *Env, cmdList []string) (verifiedCmds []string) { @@ -38,10 +51,6 @@ func (self *BreakPoints) SetAfters(cc *Cli, env *Env, cmdList []string) (verifie return } -func (self *BreakPoints) AtBegin() bool { - return self.Begin -} - func (self *BreakPoints) BreakBefore(cmd string) bool { return self.Befores[cmd] } diff --git a/pkg/cli/core/cli.go b/pkg/cli/core/cli.go index cbf15f4..2d64954 100644 --- a/pkg/cli/core/cli.go +++ b/pkg/cli/core/cli.go @@ -33,7 +33,7 @@ func (self *ExecuteMask) Copy() *ExecuteMask { } type Executor interface { - Execute(caller string, cc *Cli, env *Env, masks []*ExecuteMask, input ...string) bool + Execute(caller string, inerrCall bool, cc *Cli, env *Env, masks []*ExecuteMask, input ...string) bool Clone() Executor } @@ -73,7 +73,7 @@ func NewCli(screen Screen, cmds *CmdTree, parser CliParser, abbrs *EnvAbbrs, cmd func (self *Cli) SetFlowStatusWriter(status *ExecutingFlow) { if self.FlowStatus != nil { - panic(fmt.Errorf("[SetExecutingFlowStatusLogger] should never happen")) + panic(fmt.Errorf("[SetFlowStatusWriter] should never happen")) } self.FlowStatus = status } diff --git a/pkg/cli/core/cmd.go b/pkg/cli/core/cmd.go index 7681089..1afab3d 100644 --- a/pkg/cli/core/cmd.go +++ b/pkg/cli/core/cmd.go @@ -658,7 +658,7 @@ func (self *Cmd) executeFlow(argv ArgVals, cc *Cli, env *Env, mask *ExecuteMask) masks = mask.SubFlow } if shouldExecByMask(mask) { - succeeded = cc.Executor.Execute(self.owner.DisplayPath(), cc, flowEnv, masks, flow...) + succeeded = cc.Executor.Execute(self.owner.DisplayPath(), true, cc, flowEnv, masks, flow...) } else { cc.Screen.Print("(skipped+)\n") succeeded = true diff --git a/pkg/cli/core/cmds.go b/pkg/cli/core/cmds.go index 03991f0..9543a1a 100644 --- a/pkg/cli/core/cmds.go +++ b/pkg/cli/core/cmds.go @@ -131,6 +131,33 @@ func (self *CmdTree) MatchWriteKey(key string) bool { return self.cmd.MatchWriteKey(key) } +// TODO: slow +func (self *CmdTree) GatherNames() (names []string) { + if self.parent == nil { + names = append(names, self.DisplayPath()) + } + path := self.Path() + sep := self.Strs.PathSep + + // Real names first, then abbrs + for sub, _ := range self.subs { + names = append(names, strings.Join(append(path, sub), sep)) + } + for _, abbrs := range self.subAbbrs { + for _, abbr := range abbrs { + if _, ok := self.subs[abbr]; ok { + continue + } + names = append(names, strings.Join(append(path, abbr), sep)) + } + } + + for _, sub := range self.subs { + names = append(names, sub.GatherNames()...) + } + return names +} + func (self *CmdTree) RegCmd(cmd NormalCmd, help string) *Cmd { self.cmdConflictCheck(help, "RegCmd") self.cmd = NewCmd(self, help, cmd) diff --git a/pkg/cli/display/flow.go b/pkg/cli/display/flow.go index 77cdcba..3724619 100644 --- a/pkg/cli/display/flow.go +++ b/pkg/cli/display/flow.go @@ -725,10 +725,10 @@ func dumpExecutedModifiedEnv( // TODO: // return } - if !args.ShowExecutedModifiedEnv && executedCmd.Result == core.ExecutedResultSucceeded { + if !args.ShowExecutedModifiedEnv && executedCmd.Result != core.ExecutedResultError { return } - if executedCmd.FinishEnv == nil && executedCmd.Result != core.ExecutedResultSucceeded { + if executedCmd.FinishEnv == nil && executedCmd.Result != core.ExecutedResultError { return } diff --git a/pkg/cli/display/render.go b/pkg/cli/display/render.go index 399d9a7..71d390f 100644 --- a/pkg/cli/display/render.go +++ b/pkg/cli/display/render.go @@ -156,15 +156,19 @@ func PrintSwitchingThreadDisplay(preTid string, info core.BgTaskInfo, env *core. func getFrameChars(env *core.Env) *FrameChars { name := strings.ToLower(env.Get("display.style").Raw) chars := getFrameCharsByName(env, name) - if env.GetInt("display.executor.displayed") == env.GetInt("sys.stack-depth") { - if env.GetBool("display.utf8") { - return FrameCharsAscii() - } else { - // TODO: have bugs - //return FrameCharsNoBorder() - return FrameCharsAscii() + // TODO: have bug + /* + // Display a lite frame under another frame + if env.GetInt("display.executor.displayed") == env.GetInt("sys.stack-depth") { + if env.GetBool("display.utf8") { + return FrameCharsAscii() + } else { + // TODO: have bugs + //return FrameCharsNoBorder() + return FrameCharsAscii() + } } - } + */ return chars } diff --git a/pkg/cli/execute/cmd_type.go b/pkg/cli/execute/cmd_type.go index d9c619b..56c4441 100644 --- a/pkg/cli/execute/cmd_type.go +++ b/pkg/cli/execute/cmd_type.go @@ -102,6 +102,7 @@ func noSessionCmds(flow *core.ParsedCmds) bool { } funcs := []interface{}{ + builtin.DbgInteract, builtin.SessionRetry, //builtin.LastSessionRetry, } diff --git a/pkg/cli/execute/executor.go b/pkg/cli/execute/executor.go index 667c52c..33e9f7b 100644 --- a/pkg/cli/execute/executor.go +++ b/pkg/cli/execute/executor.go @@ -53,6 +53,9 @@ func (self *Executor) Run(cc *core.Cli, env *core.Env, bootstrap string, input . return false } ok := self.execute(self.callerNameEntry, cc, env, nil, false, false, input...) + + tryBreakAtEnd(cc, env) + builtin.WaitAllBgTasks(cc, env) if cc.FlowStatus != nil { cc.FlowStatus.OnFlowFinish(ok) @@ -61,8 +64,8 @@ func (self *Executor) Run(cc *core.Cli, env *core.Env, bootstrap string, input . } // Implement core.Executor -func (self *Executor) Execute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) bool { - return self.execute(caller, cc, env, masks, false, true, input...) +func (self *Executor) Execute(caller string, innerCall bool, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) bool { + return self.execute(caller, cc, env, masks, false, innerCall, input...) } func (self *Executor) execute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, @@ -108,7 +111,7 @@ func (self *Executor) execute(caller string, cc *core.Cli, env *core.Env, masks display.PrintTolerableErrs(cc.Screen, env, cc.TolerableErrs) - if !innerCall && !bootstrap { + if !innerCall && !bootstrap && !env.GetBool("sys.interact.inside") { noSession := noSessionCmds(flow) if !noSession { statusWriter, ok := core.SessionInit(cc, flow, env, self.sessionFileName, self.sessionStatusFileName) @@ -393,8 +396,11 @@ func stackStepOut(caller string, callerNameEntry string, env *core.Env) { if stack == callerNameEntry { stack = "" } else { - panic(fmt.Errorf("stack string not match when stepping out from '%s', stack: '%s'", - caller, stack)) + fields := strings.Split(stack, sep) + if len(fields) != 1 || fields[0] != caller { + panic(fmt.Errorf("stack string not match when stepping out from '%s', stack: '%s'", + sep+caller, stack)) + } } } else { stack = stack[0 : len(stack)-len(sep)-len(caller)] diff --git a/pkg/cli/execute/interactive.go b/pkg/cli/execute/interactive.go index c9a2ae8..5c0d464 100644 --- a/pkg/cli/execute/interactive.go +++ b/pkg/cli/execute/interactive.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/pingcap/ticat/pkg/builtin" "github.com/pingcap/ticat/pkg/cli/core" "github.com/pingcap/ticat/pkg/cli/display" ) @@ -46,7 +47,7 @@ func tryDelayAndStepByStepAndBreakBefore(cc *core.Cli, env *core.Env, cmd core.P } func tryStepByStepAndBreakBefore(cc *core.Cli, env *core.Env, cmd core.ParsedCmd, breakByPrev bool) BreakPointAction { - atBegin := cc.BreakPoints.AtBegin() + atBegin := cc.BreakPoints.BreakAtBegin() stepByStep := env.GetBool("sys.step-by-step") stepIn := env.GetBool("sys.breakpoint.status.step-in") stepOut := env.GetBool("sys.breakpoint.status.step-out") @@ -115,13 +116,30 @@ func tryBreakAfter(cc *core.Cli, env *core.Env, cmd core.ParsedCmd) BreakPointAc reason := display.ColorTip("break-point: after command ", env) + display.ColorCmd("["+name+"]", env) return readUserBPAChoice( reason, - []string{"c", "q"}, + []string{"c", "i", "q"}, getAllBPAs(), true, cc, env) } +func tryBreakAtEnd(cc *core.Cli, env *core.Env) { + if !cc.BreakPoints.BreakAtEnd() { + return + } + reason := display.ColorTip("break-point: at main-thread end", env) + bpa := readUserBPAChoice( + reason, + []string{"c", "i", "q"}, + getAllBPAs(), + true, + cc, + env) + if bpa != BPAContinue { + panic(fmt.Errorf("[tryBreakAtEnd] should never happen")) + } +} + func tryDelay(cc *core.Cli, env *core.Env, delayKey string) { delaySec := env.GetInt(delayKey) if delaySec > 0 { @@ -171,7 +189,8 @@ func readUserBPAChoice(reason string, choices []string, actions BPAs, lowerInput if action == BPAQuit { panic(core.NewAbortByUserErr()) } else if action == BPAInteract { - interactiveMode(cc, env, "e") + cc.Screen.Print("\n") + builtin.InteractiveMode(cc, env, "e") if env.GetBool("sys.interact.leaving") { env.GetLayer(core.EnvLayerSession).Delete("sys.interact.leaving") return BPAContinue @@ -186,39 +205,6 @@ func readUserBPAChoice(reason string, choices []string, actions BPAs, lowerInput } } -func interactiveMode(cc *core.Cli, env *core.Env, exitStr string) { - sessionEnv := env.GetLayer(core.EnvLayerSession) - sessionEnv.SetBool("sys.interact.inside", true) - - cc = cc.CopyForInteract() - buf := bufio.NewReader(os.Stdin) - for { - if env.GetBool("sys.interact.leaving") { - break - } - selfName := env.GetRaw("strs.self-name") - cc.Screen.Print("\n" + display.ColorExplain("", env) + display.ColorWarn(exitStr, env) + ":" + - //display.ColorExplain(" exit interactive mode\n", env)) - " exit interactive mode\n") - - cc.Screen.Print(display.ColorTip(selfName+"> ", env)) - lineBytes, err := buf.ReadBytes('\n') - if err != nil { - panic(fmt.Errorf("[readFromStdin] read from stdin failed: %v", err)) - } - if len(lineBytes) == 0 { - continue - } - line := strings.TrimSpace(string(lineBytes)) - if line == exitStr { - break - } - cc.Executor.Execute("(interact)", cc, env, nil, strings.Fields(line)...) - } - - sessionEnv.GetLayer(core.EnvLayerSession).Delete("sys.interact.inside") -} - func getAllBPAs() BPAs { return BPAs{ "c": BPAContinue,