github.com/undoio/delve@v1.9.0/pkg/terminal/terminal.go (about)

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