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 }