github.com/cnboonhan/delve@v0.0.0-20230908061759-363f2388c2fb/pkg/terminal/terminal.go (about)

     1  package terminal
     2  
     3  //lint:file-ignore ST1005 errors here can be capitalized
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"net/rpc"
     9  	"os"
    10  	"os/signal"
    11  	"strings"
    12  	"sync"
    13  	"syscall"
    14  
    15  	"github.com/derekparker/trie"
    16  	"github.com/go-delve/liner"
    17  
    18  	"github.com/go-delve/delve/pkg/config"
    19  	"github.com/go-delve/delve/pkg/locspec"
    20  	"github.com/go-delve/delve/pkg/terminal/colorize"
    21  	"github.com/go-delve/delve/pkg/terminal/starbind"
    22  	"github.com/go-delve/delve/service"
    23  	"github.com/go-delve/delve/service/api"
    24  )
    25  
    26  const (
    27  	historyFile                 string = ".dbg_history"
    28  	terminalHighlightEscapeCode string = "\033[%2dm"
    29  	terminalResetEscapeCode     string = "\033[0m"
    30  )
    31  
    32  const (
    33  	ansiBlack     = 30
    34  	ansiRed       = 31
    35  	ansiGreen     = 32
    36  	ansiYellow    = 33
    37  	ansiBlue      = 34
    38  	ansiMagenta   = 35
    39  	ansiCyan      = 36
    40  	ansiWhite     = 37
    41  	ansiBrBlack   = 90
    42  	ansiBrRed     = 91
    43  	ansiBrGreen   = 92
    44  	ansiBrYellow  = 93
    45  	ansiBrBlue    = 94
    46  	ansiBrMagenta = 95
    47  	ansiBrCyan    = 96
    48  	ansiBrWhite   = 97
    49  )
    50  
    51  // Term represents the terminal running dlv.
    52  type Term struct {
    53  	client   service.Client
    54  	conf     *config.Config
    55  	prompt   string
    56  	line     *liner.State
    57  	cmds     *Commands
    58  	stdout   *transcriptWriter
    59  	InitFile string
    60  	displays []displayEntry
    61  	oldPid   int
    62  
    63  	historyFile *os.File
    64  
    65  	starlarkEnv *starbind.Env
    66  
    67  	substitutePathRulesCache [][2]string
    68  
    69  	// quitContinue is set to true by exitCommand to signal that the process
    70  	// should be resumed before quitting.
    71  	quitContinue bool
    72  
    73  	longCommandMu         sync.Mutex
    74  	longCommandCancelFlag bool
    75  
    76  	quittingMutex sync.Mutex
    77  	quitting      bool
    78  
    79  	traceNonInteractive bool
    80  }
    81  
    82  type displayEntry struct {
    83  	expr   string
    84  	fmtstr string
    85  }
    86  
    87  // New returns a new Term.
    88  func New(client service.Client, conf *config.Config) *Term {
    89  	cmds := DebugCommands(client)
    90  	if conf != nil && conf.Aliases != nil {
    91  		cmds.Merge(conf.Aliases)
    92  	}
    93  
    94  	if conf == nil {
    95  		conf = &config.Config{}
    96  	}
    97  
    98  	t := &Term{
    99  		client: client,
   100  		conf:   conf,
   101  		prompt: "(dlv) ",
   102  		line:   liner.NewLiner(),
   103  		cmds:   cmds,
   104  		stdout: &transcriptWriter{pw: &pagingWriter{w: os.Stdout}},
   105  	}
   106  	t.line.SetCtrlZStop(true)
   107  
   108  	if strings.ToLower(os.Getenv("TERM")) != "dumb" {
   109  		t.stdout.pw = &pagingWriter{w: getColorableWriter()}
   110  		t.stdout.colorEscapes = make(map[colorize.Style]string)
   111  		t.stdout.colorEscapes[colorize.NormalStyle] = terminalResetEscapeCode
   112  	}
   113  
   114  	t.updateConfig()
   115  
   116  	if client != nil {
   117  		lcfg := t.loadConfig()
   118  		client.SetReturnValuesLoadConfig(&lcfg)
   119  		if state, err := client.GetState(); err == nil {
   120  			t.oldPid = state.Pid
   121  		}
   122  	}
   123  
   124  	t.starlarkEnv = starbind.New(starlarkContext{t}, t.stdout)
   125  	return t
   126  }
   127  
   128  func (t *Term) updateConfig() {
   129  	// These are always called together.
   130  	t.updateColorScheme()
   131  	t.updateTab()
   132  }
   133  
   134  func (t *Term) updateColorScheme() {
   135  	if t.stdout.colorEscapes == nil {
   136  		return
   137  	}
   138  
   139  	conf := t.conf
   140  	wd := func(s string, defaultCode int) string {
   141  		if s == "" {
   142  			return fmt.Sprintf(terminalHighlightEscapeCode, defaultCode)
   143  		}
   144  		return s
   145  	}
   146  	t.stdout.colorEscapes[colorize.KeywordStyle] = conf.SourceListKeywordColor
   147  	t.stdout.colorEscapes[colorize.StringStyle] = wd(conf.SourceListStringColor, ansiGreen)
   148  	t.stdout.colorEscapes[colorize.NumberStyle] = conf.SourceListNumberColor
   149  	t.stdout.colorEscapes[colorize.CommentStyle] = wd(conf.SourceListCommentColor, ansiBrMagenta)
   150  	t.stdout.colorEscapes[colorize.ArrowStyle] = wd(conf.SourceListArrowColor, ansiYellow)
   151  	t.stdout.colorEscapes[colorize.TabStyle] = wd(conf.SourceListTabColor, ansiBrBlack)
   152  	switch x := conf.SourceListLineColor.(type) {
   153  	case string:
   154  		t.stdout.colorEscapes[colorize.LineNoStyle] = x
   155  	case int:
   156  		if (x > ansiWhite && x < ansiBrBlack) || x < ansiBlack || x > ansiBrWhite {
   157  			x = ansiBlue
   158  		}
   159  		t.stdout.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, x)
   160  	case nil:
   161  		t.stdout.colorEscapes[colorize.LineNoStyle] = fmt.Sprintf(terminalHighlightEscapeCode, ansiBlue)
   162  	}
   163  }
   164  
   165  func (t *Term) updateTab() {
   166  	t.stdout.altTabString = t.conf.Tab
   167  }
   168  
   169  func (t *Term) SetTraceNonInteractive() {
   170  	t.traceNonInteractive = true
   171  }
   172  
   173  func (t *Term) IsTraceNonInteractive() bool {
   174  	return t.traceNonInteractive
   175  }
   176  
   177  // Close returns the terminal to its previous mode.
   178  func (t *Term) Close() {
   179  	t.line.Close()
   180  	if err := t.stdout.CloseTranscript(); err != nil {
   181  		fmt.Fprintf(os.Stderr, "error closing transcript file: %v\n", err)
   182  	}
   183  }
   184  
   185  func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
   186  	for range ch {
   187  		t.longCommandCancel()
   188  		t.starlarkEnv.Cancel()
   189  		state, err := t.client.GetStateNonBlocking()
   190  		if err == nil && state.Recording {
   191  			fmt.Fprintf(t.stdout, "received SIGINT, stopping recording (will not forward signal)\n")
   192  			err := t.client.StopRecording()
   193  			if err != nil {
   194  				fmt.Fprintf(os.Stderr, "%v\n", err)
   195  			}
   196  			continue
   197  		}
   198  		if err == nil && state.CoreDumping {
   199  			fmt.Fprintf(t.stdout, "received SIGINT, stopping dump\n")
   200  			err := t.client.CoreDumpCancel()
   201  			if err != nil {
   202  				fmt.Fprintf(os.Stderr, "%v\n", err)
   203  			}
   204  			continue
   205  		}
   206  		if multiClient {
   207  			answer, err := t.line.Prompt("Would you like to [p]ause the target (returning to Delve's prompt) or [q]uit this client (leaving the target running) [p/q]? ")
   208  			if err != nil {
   209  				fmt.Fprintf(os.Stderr, "%v", err)
   210  				continue
   211  			}
   212  			answer = strings.TrimSpace(answer)
   213  			switch answer {
   214  			case "p":
   215  				_, err := t.client.Halt()
   216  				if err != nil {
   217  					fmt.Fprintf(os.Stderr, "%v", err)
   218  				}
   219  			case "q":
   220  				t.quittingMutex.Lock()
   221  				t.quitting = true
   222  				t.quittingMutex.Unlock()
   223  				err := t.client.Disconnect(false)
   224  				if err != nil {
   225  					fmt.Fprintf(os.Stderr, "%v", err)
   226  				} else {
   227  					t.Close()
   228  				}
   229  			default:
   230  				fmt.Fprintln(t.stdout, "only p or q allowed")
   231  			}
   232  
   233  		} else {
   234  			fmt.Fprintf(t.stdout, "received SIGINT, stopping process (will not forward signal)\n")
   235  			_, err := t.client.Halt()
   236  			if err != nil {
   237  				fmt.Fprintf(t.stdout, "%v", err)
   238  			}
   239  		}
   240  	}
   241  }
   242  
   243  // Run begins running dlv in the terminal.
   244  func (t *Term) Run() (int, error) {
   245  	defer t.Close()
   246  
   247  	multiClient := t.client.IsMulticlient()
   248  
   249  	// Send the debugger a halt command on SIGINT
   250  	ch := make(chan os.Signal, 1)
   251  	signal.Notify(ch, syscall.SIGINT)
   252  	go t.sigintGuard(ch, multiClient)
   253  
   254  	fns := trie.New()
   255  	cmds := trie.New()
   256  	funcs, _ := t.client.ListFunctions("")
   257  	for _, fn := range funcs {
   258  		fns.Add(fn, nil)
   259  	}
   260  	for _, cmd := range t.cmds.cmds {
   261  		for _, alias := range cmd.aliases {
   262  			cmds.Add(alias, nil)
   263  		}
   264  	}
   265  
   266  	var locs *trie.Trie
   267  
   268  	t.line.SetCompleter(func(line string) (c []string) {
   269  		cmd := t.cmds.Find(strings.Split(line, " ")[0], noPrefix)
   270  		switch cmd.aliases[0] {
   271  		case "break", "trace", "continue":
   272  			if spc := strings.LastIndex(line, " "); spc > 0 {
   273  				prefix := line[:spc] + " "
   274  				funcs := fns.FuzzySearch(line[spc+1:])
   275  				for _, f := range funcs {
   276  					c = append(c, prefix+f)
   277  				}
   278  			}
   279  		case "nullcmd", "nocmd":
   280  			commands := cmds.FuzzySearch(strings.ToLower(line))
   281  			c = append(c, commands...)
   282  		case "print", "whatis":
   283  			if locs == nil {
   284  				localVars, err := t.client.ListLocalVariables(
   285  					api.EvalScope{GoroutineID: -1, Frame: t.cmds.frame, DeferredCall: 0},
   286  					api.LoadConfig{},
   287  				)
   288  				if err != nil {
   289  					fmt.Fprintf(os.Stderr, "Unable to get local variables: %s\n", err)
   290  					break
   291  				}
   292  
   293  				locs = trie.New()
   294  				for _, loc := range localVars {
   295  					locs.Add(loc.Name, nil)
   296  				}
   297  			}
   298  
   299  			if spc := strings.LastIndex(line, " "); spc > 0 {
   300  				prefix := line[:spc] + " "
   301  				locals := locs.FuzzySearch(line[spc+1:])
   302  				for _, l := range locals {
   303  					c = append(c, prefix+l)
   304  				}
   305  			}
   306  		}
   307  		return
   308  	})
   309  
   310  	fullHistoryFile, err := config.GetConfigFilePath(historyFile)
   311  	if err != nil {
   312  		fmt.Printf("Unable to load history file: %v.", err)
   313  	}
   314  
   315  	t.historyFile, err = os.OpenFile(fullHistoryFile, os.O_RDWR|os.O_CREATE, 0600)
   316  	if err != nil {
   317  		fmt.Printf("Unable to open history file: %v. History will not be saved for this session.", err)
   318  	}
   319  	if _, err := t.line.ReadHistory(t.historyFile); err != nil {
   320  		fmt.Printf("Unable to read history file %s: %v\n", fullHistoryFile, err)
   321  	}
   322  
   323  	fmt.Println("Type 'help' for list of commands.")
   324  
   325  	if t.InitFile != "" {
   326  		err := t.cmds.executeFile(t, t.InitFile)
   327  		if err != nil {
   328  			if _, ok := err.(ExitRequestError); ok {
   329  				return t.handleExit()
   330  			}
   331  			fmt.Fprintf(os.Stderr, "Error executing init file: %s\n", err)
   332  		}
   333  	}
   334  
   335  	var lastCmd string
   336  
   337  	// Ensure that the target process is neither running nor recording by
   338  	// making a blocking call.
   339  	_, _ = t.client.GetState()
   340  
   341  	for {
   342  		locs = nil
   343  
   344  		cmdstr, err := t.promptForInput()
   345  		if err != nil {
   346  			if err == io.EOF {
   347  				fmt.Fprintln(t.stdout, "exit")
   348  				return t.handleExit()
   349  			}
   350  			return 1, fmt.Errorf("Prompt for input failed.\n")
   351  		}
   352  		t.stdout.Echo(t.prompt + cmdstr + "\n")
   353  
   354  		if strings.TrimSpace(cmdstr) == "" {
   355  			cmdstr = lastCmd
   356  		}
   357  
   358  		lastCmd = cmdstr
   359  
   360  		if err := t.cmds.Call(cmdstr, t); err != nil {
   361  			if _, ok := err.(ExitRequestError); ok {
   362  				return t.handleExit()
   363  			}
   364  			// The type information gets lost in serialization / de-serialization,
   365  			// so we do a string compare on the error message to see if the process
   366  			// has exited, or if the command actually failed.
   367  			if strings.Contains(err.Error(), "exited") {
   368  				fmt.Fprintln(os.Stderr, err.Error())
   369  			} else {
   370  				t.quittingMutex.Lock()
   371  				quitting := t.quitting
   372  				t.quittingMutex.Unlock()
   373  				if quitting {
   374  					return t.handleExit()
   375  				}
   376  				fmt.Fprintf(os.Stderr, "Command failed: %s\n", err)
   377  			}
   378  		}
   379  
   380  		t.stdout.Flush()
   381  		t.stdout.pw.Reset()
   382  	}
   383  }
   384  
   385  // Substitutes directory to source file.
   386  //
   387  // Ensures that only directory is substituted, for example:
   388  // substitute from `/dir/subdir`, substitute to `/new`
   389  // for file path `/dir/subdir/file` will return file path `/new/file`.
   390  // for file path `/dir/subdir-2/file` substitution will not be applied.
   391  //
   392  // If more than one substitution rule is defined, the rules are applied
   393  // in the order they are defined, first rule that matches is used for
   394  // substitution.
   395  func (t *Term) substitutePath(path string) string {
   396  	if t.conf == nil {
   397  		return path
   398  	}
   399  	return locspec.SubstitutePath(path, t.substitutePathRules())
   400  }
   401  
   402  func (t *Term) substitutePathRules() [][2]string {
   403  	if t.substitutePathRulesCache != nil {
   404  		return t.substitutePathRulesCache
   405  	}
   406  	if t.conf == nil || t.conf.SubstitutePath == nil {
   407  		return nil
   408  	}
   409  	spr := make([][2]string, 0, len(t.conf.SubstitutePath))
   410  	for _, r := range t.conf.SubstitutePath {
   411  		spr = append(spr, [2]string{r.From, r.To})
   412  	}
   413  	t.substitutePathRulesCache = spr
   414  	return spr
   415  }
   416  
   417  // formatPath applies path substitution rules and shortens the resulting
   418  // path by replacing the current directory with './'
   419  func (t *Term) formatPath(path string) string {
   420  	path = t.substitutePath(path)
   421  	workingDir, _ := os.Getwd()
   422  	return strings.Replace(path, workingDir, ".", 1)
   423  }
   424  
   425  func (t *Term) promptForInput() (string, error) {
   426  	l, err := t.line.Prompt(t.prompt)
   427  	if err != nil {
   428  		return "", err
   429  	}
   430  
   431  	l = strings.TrimSuffix(l, "\n")
   432  	if l != "" {
   433  		t.line.AppendHistory(l)
   434  	}
   435  
   436  	return l, nil
   437  }
   438  
   439  func yesno(line *liner.State, question, defaultAnswer string) (bool, error) {
   440  	for {
   441  		answer, err := line.Prompt(question)
   442  		if err != nil {
   443  			return false, err
   444  		}
   445  		answer = strings.ToLower(strings.TrimSpace(answer))
   446  		if answer == "" {
   447  			answer = defaultAnswer
   448  		}
   449  		switch answer {
   450  		case "n", "no":
   451  			return false, nil
   452  		case "y", "yes":
   453  			return true, nil
   454  		}
   455  	}
   456  }
   457  
   458  func (t *Term) handleExit() (int, error) {
   459  	if t.historyFile != nil {
   460  		if _, err := t.line.WriteHistory(t.historyFile); err != nil {
   461  			fmt.Println("readline history error:", err)
   462  		}
   463  		if err := t.historyFile.Close(); err != nil {
   464  			fmt.Printf("error closing history file: %s\n", err)
   465  		}
   466  	}
   467  
   468  	t.quittingMutex.Lock()
   469  	quitting := t.quitting
   470  	t.quittingMutex.Unlock()
   471  	if quitting {
   472  		return 0, nil
   473  	}
   474  
   475  	s, err := t.client.GetState()
   476  	if err != nil {
   477  		if isErrProcessExited(err) {
   478  			if t.client.IsMulticlient() {
   479  				answer, err := yesno(t.line, "Remote process has exited. Would you like to kill the headless instance? [Y/n] ", "yes")
   480  				if err != nil {
   481  					return 2, io.EOF
   482  				}
   483  				if answer {
   484  					if err := t.client.Detach(true); err != nil {
   485  						return 1, err
   486  					}
   487  				}
   488  				return 0, err
   489  			}
   490  			return 0, nil
   491  		}
   492  		return 1, err
   493  	}
   494  	if !s.Exited {
   495  		if t.quitContinue {
   496  			err := t.client.Disconnect(true)
   497  			if err != nil {
   498  				return 2, err
   499  			}
   500  			return 0, nil
   501  		}
   502  
   503  		doDetach := true
   504  		if t.client.IsMulticlient() {
   505  			answer, err := yesno(t.line, "Would you like to kill the headless instance? [Y/n] ", "yes")
   506  			if err != nil {
   507  				return 2, io.EOF
   508  			}
   509  			doDetach = answer
   510  		}
   511  
   512  		if doDetach {
   513  			kill := true
   514  			if t.client.AttachedToExistingProcess() {
   515  				answer, err := yesno(t.line, "Would you like to kill the process? [Y/n] ", "yes")
   516  				if err != nil {
   517  					return 2, io.EOF
   518  				}
   519  				kill = answer
   520  			}
   521  			if err := t.client.Detach(kill); err != nil {
   522  				return 1, err
   523  			}
   524  		}
   525  	}
   526  	return 0, nil
   527  }
   528  
   529  // loadConfig returns an api.LoadConfig with the parameters specified in
   530  // the configuration file.
   531  func (t *Term) loadConfig() api.LoadConfig {
   532  	r := api.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
   533  
   534  	if t.conf != nil && t.conf.MaxStringLen != nil {
   535  		r.MaxStringLen = *t.conf.MaxStringLen
   536  	}
   537  	if t.conf != nil && t.conf.MaxArrayValues != nil {
   538  		r.MaxArrayValues = *t.conf.MaxArrayValues
   539  	}
   540  	if t.conf != nil && t.conf.MaxVariableRecurse != nil {
   541  		r.MaxVariableRecurse = *t.conf.MaxVariableRecurse
   542  	}
   543  
   544  	return r
   545  }
   546  
   547  func (t *Term) removeDisplay(n int) error {
   548  	if n < 0 || n >= len(t.displays) {
   549  		return fmt.Errorf("%d is out of range", n)
   550  	}
   551  	t.displays[n] = displayEntry{"", ""}
   552  	for i := len(t.displays) - 1; i >= 0; i-- {
   553  		if t.displays[i].expr != "" {
   554  			t.displays = t.displays[:i+1]
   555  			return nil
   556  		}
   557  	}
   558  	t.displays = t.displays[:0]
   559  	return nil
   560  }
   561  
   562  func (t *Term) addDisplay(expr, fmtstr string) {
   563  	t.displays = append(t.displays, displayEntry{expr: expr, fmtstr: fmtstr})
   564  }
   565  
   566  func (t *Term) printDisplay(i int) {
   567  	expr, fmtstr := t.displays[i].expr, t.displays[i].fmtstr
   568  	val, err := t.client.EvalVariable(api.EvalScope{GoroutineID: -1}, expr, ShortLoadConfig)
   569  	if err != nil {
   570  		if isErrProcessExited(err) {
   571  			return
   572  		}
   573  		fmt.Fprintf(t.stdout, "%d: %s = error %v\n", i, expr, err)
   574  		return
   575  	}
   576  	fmt.Fprintf(t.stdout, "%d: %s = %s\n", i, val.Name, val.SinglelineStringFormatted(fmtstr))
   577  }
   578  
   579  func (t *Term) printDisplays() {
   580  	for i := range t.displays {
   581  		if t.displays[i].expr != "" {
   582  			t.printDisplay(i)
   583  		}
   584  	}
   585  }
   586  
   587  func (t *Term) onStop() {
   588  	t.printDisplays()
   589  }
   590  
   591  func (t *Term) longCommandCancel() {
   592  	t.longCommandMu.Lock()
   593  	defer t.longCommandMu.Unlock()
   594  	t.longCommandCancelFlag = true
   595  }
   596  
   597  func (t *Term) longCommandStart() {
   598  	t.longCommandMu.Lock()
   599  	defer t.longCommandMu.Unlock()
   600  	t.longCommandCancelFlag = false
   601  }
   602  
   603  func (t *Term) longCommandCanceled() bool {
   604  	t.longCommandMu.Lock()
   605  	defer t.longCommandMu.Unlock()
   606  	return t.longCommandCancelFlag
   607  }
   608  
   609  // RedirectTo redirects the output of this terminal to the specified writer.
   610  func (t *Term) RedirectTo(w io.Writer) {
   611  	t.stdout.pw.w = w
   612  }
   613  
   614  // isErrProcessExited returns true if `err` is an RPC error equivalent of proc.ErrProcessExited
   615  func isErrProcessExited(err error) bool {
   616  	rpcError, ok := err.(rpc.ServerError)
   617  	return ok && strings.Contains(rpcError.Error(), "has exited with status")
   618  }