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 }