github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/ssa/debug_test.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package ssa_test 6 7 import ( 8 "flag" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strconv" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/go-asm/go/testenv" 22 ) 23 24 var ( 25 update = flag.Bool("u", false, "update test reference files") 26 verbose = flag.Bool("v", false, "print debugger interactions (very verbose)") 27 dryrun = flag.Bool("n", false, "just print the command line and first debugging bits") 28 useGdb = flag.Bool("g", false, "use Gdb instead of Delve (dlv), use gdb reference files") 29 force = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir") 30 repeats = flag.Bool("r", false, "detect repeats in debug steps and don't ignore them") 31 inlines = flag.Bool("i", false, "do inlining for gdb (makes testing flaky till inlining info is correct)") 32 ) 33 34 var ( 35 hexRe = regexp.MustCompile("0x[a-zA-Z0-9]+") 36 numRe = regexp.MustCompile("-?\\d+") 37 stringRe = regexp.MustCompile("\"([^\\\"]|(\\.))*\"") 38 leadingDollarNumberRe = regexp.MustCompile("^[$]\\d+") 39 optOutGdbRe = regexp.MustCompile("[<]optimized out[>]") 40 numberColonRe = regexp.MustCompile("^ *\\d+:") 41 ) 42 43 var gdb = "gdb" // Might be "ggdb" on Darwin, because gdb no longer part of XCode 44 var debugger = "dlv" // For naming files, etc. 45 46 var gogcflags = os.Getenv("GO_GCFLAGS") 47 48 // optimizedLibs usually means "not running in a noopt test builder". 49 var optimizedLibs = (!strings.Contains(gogcflags, "-N") && !strings.Contains(gogcflags, "-l")) 50 51 // TestNexting go-builds a file, then uses a debugger (default delve, optionally gdb) 52 // to next through the generated executable, recording each line landed at, and 53 // then compares those lines with reference file(s). 54 // Flag -u updates the reference file(s). 55 // Flag -g changes the debugger to gdb (and uses gdb-specific reference files) 56 // Flag -v is ever-so-slightly verbose. 57 // Flag -n is for dry-run, and prints the shell and first debug commands. 58 // 59 // Because this test (combined with existing compiler deficiencies) is flaky, 60 // for gdb-based testing by default inlining is disabled 61 // (otherwise output depends on library internals) 62 // and for both gdb and dlv by default repeated lines in the next stream are ignored 63 // (because this appears to be timing-dependent in gdb, and the cleanest fix is in code common to gdb and dlv). 64 // 65 // Also by default, any source code outside of .../testdata/ is not mentioned 66 // in the debugging histories. This deals both with inlined library code once 67 // the compiler is generating clean inline records, and also deals with 68 // runtime code between return from main and process exit. This is hidden 69 // so that those files (in the runtime/library) can change without affecting 70 // this test. 71 // 72 // These choices can be reversed with -i (inlining on) and -r (repeats detected) which 73 // will also cause their own failures against the expected outputs. Note that if the compiler 74 // and debugger were behaving properly, the inlined code and repeated lines would not appear, 75 // so the expected output is closer to what we hope to see, though it also encodes all our 76 // current bugs. 77 // 78 // The file being tested may contain comments of the form 79 // //DBG-TAG=(v1,v2,v3) 80 // where DBG = {gdb,dlv} and TAG={dbg,opt} 81 // each variable may optionally be followed by a / and one or more of S,A,N,O 82 // to indicate normalization of Strings, (hex) addresses, and numbers. 83 // "O" is an explicit indication that we expect it to be optimized out. 84 // For example: 85 // 86 // if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A) 87 // 88 // TODO: not implemented for Delve yet, but this is the plan 89 // 90 // After a compiler change that causes a difference in the debug behavior, check 91 // to see if it is sensible or not, and if it is, update the reference files with 92 // go test debug_test.go -args -u 93 // (for Delve) 94 // go test debug_test.go -args -u -d 95 func TestNexting(t *testing.T) { 96 testenv.SkipFlaky(t, 37404) 97 98 skipReasons := "" // Many possible skip reasons, list all that apply 99 if testing.Short() { 100 skipReasons = "not run in short mode; " 101 } 102 testenv.MustHaveGoBuild(t) 103 104 if *useGdb && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") { 105 // Running gdb on OSX/darwin is very flaky. 106 // Sometimes it is called ggdb, depending on how it is installed. 107 // It also sometimes requires an admin password typed into a dialog box. 108 // Various architectures tend to differ slightly sometimes, and keeping them 109 // all in sync is a pain for people who don't have them all at hand, 110 // so limit testing to amd64 (for now) 111 skipReasons += "not run when testing gdb (-g) unless forced (-f) or linux-amd64; " 112 } 113 114 if !*useGdb && !*force && testenv.Builder() == "linux-386-longtest" { 115 // The latest version of Delve does support linux/386. However, the version currently 116 // installed in the linux-386-longtest builder does not. See golang.org/issue/39309. 117 skipReasons += "not run when testing delve on linux-386-longtest builder unless forced (-f); " 118 } 119 120 if *useGdb { 121 debugger = "gdb" 122 _, err := exec.LookPath(gdb) 123 if err != nil { 124 if runtime.GOOS != "darwin" { 125 skipReasons += "not run because gdb not on path; " 126 } else { 127 // On Darwin, MacPorts installs gdb as "ggdb". 128 _, err = exec.LookPath("ggdb") 129 if err != nil { 130 skipReasons += "not run because gdb (and also ggdb) request by -g option not on path; " 131 } else { 132 gdb = "ggdb" 133 } 134 } 135 } 136 } else { // Delve 137 debugger = "dlv" 138 _, err := exec.LookPath("dlv") 139 if err != nil { 140 skipReasons += "not run because dlv not on path; " 141 } 142 } 143 144 if skipReasons != "" { 145 t.Skip(skipReasons[:len(skipReasons)-2]) 146 } 147 148 optFlags := "" // Whatever flags are needed to test debugging of optimized code. 149 dbgFlags := "-N -l" 150 if *useGdb && !*inlines { 151 // For gdb (default), disable inlining so that a compiler test does not depend on library code. 152 // TODO: Technically not necessary in 1.10 and later, but it causes a largish regression that needs investigation. 153 optFlags += " -l" 154 } 155 156 moreargs := []string{} 157 if *useGdb && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") { 158 // gdb and lldb on Darwin do not deal with compressed dwarf. 159 // also, Windows. 160 moreargs = append(moreargs, "-ldflags=-compressdwarf=false") 161 } 162 163 subTest(t, debugger+"-dbg", "hist", dbgFlags, moreargs...) 164 subTest(t, debugger+"-dbg", "scopes", dbgFlags, moreargs...) 165 subTest(t, debugger+"-dbg", "i22558", dbgFlags, moreargs...) 166 167 subTest(t, debugger+"-dbg-race", "i22600", dbgFlags, append(moreargs, "-race")...) 168 169 optSubTest(t, debugger+"-opt", "hist", optFlags, 1000, moreargs...) 170 optSubTest(t, debugger+"-opt", "scopes", optFlags, 1000, moreargs...) 171 172 // Was optSubtest, this test is observed flaky on Linux in Docker on (busy) macOS, probably because of timing 173 // glitches in this harness. 174 // TODO get rid of timing glitches in this harness. 175 skipSubTest(t, debugger+"-opt", "infloop", optFlags, 10, moreargs...) 176 177 } 178 179 // subTest creates a subtest that compiles basename.go with the specified gcflags and additional compiler arguments, 180 // then runs the debugger on the resulting binary, with any comment-specified actions matching tag triggered. 181 func subTest(t *testing.T, tag string, basename string, gcflags string, moreargs ...string) { 182 t.Run(tag+"-"+basename, func(t *testing.T) { 183 if t.Name() == "TestNexting/gdb-dbg-i22558" { 184 testenv.SkipFlaky(t, 31263) 185 } 186 testNexting(t, basename, tag, gcflags, 1000, moreargs...) 187 }) 188 } 189 190 // skipSubTest is the same as subTest except that it skips the test if execution is not forced (-f) 191 func skipSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) { 192 t.Run(tag+"-"+basename, func(t *testing.T) { 193 if *force { 194 testNexting(t, basename, tag, gcflags, count, moreargs...) 195 } else { 196 t.Skip("skipping flaky test becaused not forced (-f)") 197 } 198 }) 199 } 200 201 // optSubTest is the same as subTest except that it skips the test if the runtime and libraries 202 // were not compiled with optimization turned on. (The skip may not be necessary with Go 1.10 and later) 203 func optSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) { 204 // If optimized test is run with unoptimized libraries (compiled with -N -l), it is very likely to fail. 205 // This occurs in the noopt builders (for example). 206 t.Run(tag+"-"+basename, func(t *testing.T) { 207 if *force || optimizedLibs { 208 testNexting(t, basename, tag, gcflags, count, moreargs...) 209 } else { 210 t.Skip("skipping for unoptimized stdlib/runtime") 211 } 212 }) 213 } 214 215 func testNexting(t *testing.T, base, tag, gcflags string, count int, moreArgs ...string) { 216 // (1) In testdata, build sample.go into test-sample.<tag> 217 // (2) Run debugger gathering a history 218 // (3) Read expected history from testdata/sample.<tag>.nexts 219 // optionally, write out testdata/sample.<tag>.nexts 220 221 testbase := filepath.Join("testdata", base) + "." + tag 222 tmpbase := filepath.Join("testdata", "test-"+base+"."+tag) 223 224 // Use a temporary directory unless -f is specified 225 if !*force { 226 tmpdir := t.TempDir() 227 tmpbase = filepath.Join(tmpdir, "test-"+base+"."+tag) 228 if *verbose { 229 fmt.Printf("Tempdir is %s\n", tmpdir) 230 } 231 } 232 exe := tmpbase 233 234 runGoArgs := []string{"build", "-o", exe, "-gcflags=all=" + gcflags} 235 runGoArgs = append(runGoArgs, moreArgs...) 236 runGoArgs = append(runGoArgs, filepath.Join("testdata", base+".go")) 237 238 runGo(t, "", runGoArgs...) 239 240 nextlog := testbase + ".nexts" 241 tmplog := tmpbase + ".nexts" 242 var dbg dbgr 243 if *useGdb { 244 dbg = newGdb(t, tag, exe) 245 } else { 246 dbg = newDelve(t, tag, exe) 247 } 248 h1 := runDbgr(dbg, count) 249 if *dryrun { 250 fmt.Printf("# Tag for above is %s\n", dbg.tag()) 251 return 252 } 253 if *update { 254 h1.write(nextlog) 255 } else { 256 h0 := &nextHist{} 257 h0.read(nextlog) 258 if !h0.equals(h1) { 259 // Be very noisy about exactly what's wrong to simplify debugging. 260 h1.write(tmplog) 261 cmd := testenv.Command(t, "diff", "-u", nextlog, tmplog) 262 line := asCommandLine("", cmd) 263 bytes, err := cmd.CombinedOutput() 264 if err != nil && len(bytes) == 0 { 265 t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err) 266 } 267 t.Fatalf("step/next histories differ, diff=\n%s", string(bytes)) 268 } 269 } 270 } 271 272 type dbgr interface { 273 start() 274 stepnext(s string) bool // step or next, possible with parameter, gets line etc. returns true for success, false for unsure response 275 quit() 276 hist() *nextHist 277 tag() string 278 } 279 280 func runDbgr(dbg dbgr, maxNext int) *nextHist { 281 dbg.start() 282 if *dryrun { 283 return nil 284 } 285 for i := 0; i < maxNext; i++ { 286 if !dbg.stepnext("n") { 287 break 288 } 289 } 290 dbg.quit() 291 h := dbg.hist() 292 return h 293 } 294 295 func runGo(t *testing.T, dir string, args ...string) string { 296 var stdout, stderr strings.Builder 297 cmd := testenv.Command(t, testenv.GoToolPath(t), args...) 298 cmd.Dir = dir 299 if *dryrun { 300 fmt.Printf("%s\n", asCommandLine("", cmd)) 301 return "" 302 } 303 cmd.Stdout = &stdout 304 cmd.Stderr = &stderr 305 306 if err := cmd.Run(); err != nil { 307 t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) 308 } 309 310 if s := stderr.String(); s != "" { 311 t.Fatalf("Stderr = %s\nWant empty", s) 312 } 313 314 return stdout.String() 315 } 316 317 // tstring provides two strings, o (stdout) and e (stderr) 318 type tstring struct { 319 o string 320 e string 321 } 322 323 func (t tstring) String() string { 324 return t.o + t.e 325 } 326 327 type pos struct { 328 line uint32 329 file uint8 // Artifact of plans to implement differencing instead of calling out to diff. 330 } 331 332 type nextHist struct { 333 f2i map[string]uint8 334 fs []string 335 ps []pos 336 texts []string 337 vars [][]string 338 } 339 340 func (h *nextHist) write(filename string) { 341 file, err := os.Create(filename) 342 if err != nil { 343 panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err)) 344 } 345 defer file.Close() 346 var lastfile uint8 347 for i, x := range h.texts { 348 p := h.ps[i] 349 if lastfile != p.file { 350 fmt.Fprintf(file, " %s\n", h.fs[p.file-1]) 351 lastfile = p.file 352 } 353 fmt.Fprintf(file, "%d:%s\n", p.line, x) 354 // TODO, normalize between gdb and dlv into a common, comparable format. 355 for _, y := range h.vars[i] { 356 y = strings.TrimSpace(y) 357 fmt.Fprintf(file, "%s\n", y) 358 } 359 } 360 file.Close() 361 } 362 363 func (h *nextHist) read(filename string) { 364 h.f2i = make(map[string]uint8) 365 bytes, err := os.ReadFile(filename) 366 if err != nil { 367 panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err)) 368 } 369 var lastfile string 370 lines := strings.Split(string(bytes), "\n") 371 for i, l := range lines { 372 if len(l) > 0 && l[0] != '#' { 373 if l[0] == ' ' { 374 // file -- first two characters expected to be " " 375 lastfile = strings.TrimSpace(l) 376 } else if numberColonRe.MatchString(l) { 377 // line number -- <number>:<line> 378 colonPos := strings.Index(l, ":") 379 if colonPos == -1 { 380 panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename)) 381 } 382 h.add(lastfile, l[0:colonPos], l[colonPos+1:]) 383 } else { 384 h.addVar(l) 385 } 386 } 387 } 388 } 389 390 // add appends file (name), line (number) and text (string) to the history, 391 // provided that the file+line combo does not repeat the previous position, 392 // and provided that the file is within the testdata directory. The return 393 // value indicates whether the append occurred. 394 func (h *nextHist) add(file, line, text string) bool { 395 // Only record source code in testdata unless the inlines flag is set 396 if !*inlines && !strings.Contains(file, "/testdata/") { 397 return false 398 } 399 fi := h.f2i[file] 400 if fi == 0 { 401 h.fs = append(h.fs, file) 402 fi = uint8(len(h.fs)) 403 h.f2i[file] = fi 404 } 405 406 line = strings.TrimSpace(line) 407 var li int 408 var err error 409 if line != "" { 410 li, err = strconv.Atoi(line) 411 if err != nil { 412 panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err)) 413 } 414 } 415 l := len(h.ps) 416 p := pos{line: uint32(li), file: fi} 417 418 if l == 0 || *repeats || h.ps[l-1] != p { 419 h.ps = append(h.ps, p) 420 h.texts = append(h.texts, text) 421 h.vars = append(h.vars, []string{}) 422 return true 423 } 424 return false 425 } 426 427 func (h *nextHist) addVar(text string) { 428 l := len(h.texts) 429 h.vars[l-1] = append(h.vars[l-1], text) 430 } 431 432 func invertMapSU8(hf2i map[string]uint8) map[uint8]string { 433 hi2f := make(map[uint8]string) 434 for hs, i := range hf2i { 435 hi2f[i] = hs 436 } 437 return hi2f 438 } 439 440 func (h *nextHist) equals(k *nextHist) bool { 441 if len(h.f2i) != len(k.f2i) { 442 return false 443 } 444 if len(h.ps) != len(k.ps) { 445 return false 446 } 447 hi2f := invertMapSU8(h.f2i) 448 ki2f := invertMapSU8(k.f2i) 449 450 for i, hs := range hi2f { 451 if hs != ki2f[i] { 452 return false 453 } 454 } 455 456 for i, x := range h.ps { 457 if k.ps[i] != x { 458 return false 459 } 460 } 461 462 for i, hv := range h.vars { 463 kv := k.vars[i] 464 if len(hv) != len(kv) { 465 return false 466 } 467 for j, hvt := range hv { 468 if hvt != kv[j] { 469 return false 470 } 471 } 472 } 473 474 return true 475 } 476 477 // canonFileName strips everything before "/src/" from a filename. 478 // This makes file names portable across different machines, 479 // home directories, and temporary directories. 480 func canonFileName(f string) string { 481 i := strings.Index(f, "/src/") 482 if i != -1 { 483 f = f[i+1:] 484 } 485 return f 486 } 487 488 /* Delve */ 489 490 type delveState struct { 491 cmd *exec.Cmd 492 tagg string 493 *ioState 494 atLineRe *regexp.Regexp // "\n =>" 495 funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)" 496 line string 497 file string 498 function string 499 } 500 501 func newDelve(t testing.TB, tag, executable string, args ...string) dbgr { 502 cmd := testenv.Command(t, "dlv", "exec", executable) 503 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 504 if len(args) > 0 { 505 cmd.Args = append(cmd.Args, "--") 506 cmd.Args = append(cmd.Args, args...) 507 } 508 s := &delveState{tagg: tag, cmd: cmd} 509 // HAHA Delve has control characters embedded to change the color of the => and the line number 510 // that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb 511 s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)") 512 s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n") 513 s.ioState = newIoState(s.cmd) 514 return s 515 } 516 517 func (s *delveState) tag() string { 518 return s.tagg 519 } 520 521 func (s *delveState) stepnext(ss string) bool { 522 x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ") 523 excerpts := s.atLineRe.FindStringSubmatch(x.o) 524 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 525 excerpt := "" 526 if len(excerpts) > 1 { 527 excerpt = excerpts[1] 528 } 529 if len(locations) > 0 { 530 fn := canonFileName(locations[2]) 531 if *verbose { 532 if s.file != fn { 533 fmt.Printf("%s\n", locations[2]) // don't canonocalize verbose logging 534 } 535 fmt.Printf(" %s\n", locations[3]) 536 } 537 s.line = locations[3] 538 s.file = fn 539 s.function = locations[1] 540 s.ioState.history.add(s.file, s.line, excerpt) 541 // TODO: here is where variable processing will be added. See gdbState.stepnext as a guide. 542 // Adding this may require some amount of normalization so that logs are comparable. 543 return true 544 } 545 if *verbose { 546 fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e) 547 } 548 return false 549 } 550 551 func (s *delveState) start() { 552 if *dryrun { 553 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 554 fmt.Printf("b main.test\n") 555 fmt.Printf("c\n") 556 return 557 } 558 err := s.cmd.Start() 559 if err != nil { 560 line := asCommandLine("", s.cmd) 561 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 562 } 563 s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.") 564 s.ioState.writeReadExpect("b main.test\n", "[(]dlv[)] ") 565 s.stepnext("c") 566 } 567 568 func (s *delveState) quit() { 569 expect("", s.ioState.writeRead("q\n")) 570 } 571 572 /* Gdb */ 573 574 type gdbState struct { 575 cmd *exec.Cmd 576 tagg string 577 args []string 578 *ioState 579 atLineRe *regexp.Regexp 580 funcFileLinePCre *regexp.Regexp 581 line string 582 file string 583 function string 584 } 585 586 func newGdb(t testing.TB, tag, executable string, args ...string) dbgr { 587 // Turn off shell, necessary for Darwin apparently 588 cmd := testenv.Command(t, gdb, "-nx", 589 "-iex", fmt.Sprintf("add-auto-load-safe-path %s/src/runtime", runtime.GOROOT()), 590 "-ex", "set startup-with-shell off", executable) 591 cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb") 592 s := &gdbState{tagg: tag, cmd: cmd, args: args} 593 s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)") 594 s.funcFileLinePCre = regexp.MustCompile( 595 "([^ ]+) [(][^)]*[)][ \\t\\n]+at ([^:]+):([0-9]+)") 596 // runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201 597 // function file line 598 // Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18 599 s.ioState = newIoState(s.cmd) 600 return s 601 } 602 603 func (s *gdbState) tag() string { 604 return s.tagg 605 } 606 607 func (s *gdbState) start() { 608 run := "run" 609 for _, a := range s.args { 610 run += " " + a // Can't quote args for gdb, it will pass them through including the quotes 611 } 612 if *dryrun { 613 fmt.Printf("%s\n", asCommandLine("", s.cmd)) 614 fmt.Printf("tbreak main.test\n") 615 fmt.Printf("%s\n", run) 616 return 617 } 618 err := s.cmd.Start() 619 if err != nil { 620 line := asCommandLine("", s.cmd) 621 panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err)) 622 } 623 s.ioState.readSimpleExpecting("[(]gdb[)] ") 624 x := s.ioState.writeReadExpect("b main.test\n", "[(]gdb[)] ") 625 expect("Breakpoint [0-9]+ at", x) 626 s.stepnext(run) 627 } 628 629 func (s *gdbState) stepnext(ss string) bool { 630 x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ") 631 excerpts := s.atLineRe.FindStringSubmatch(x.o) 632 locations := s.funcFileLinePCre.FindStringSubmatch(x.o) 633 excerpt := "" 634 addedLine := false 635 if len(excerpts) == 0 && len(locations) == 0 { 636 if *verbose { 637 fmt.Printf("DID NOT MATCH %s", x.o) 638 } 639 return false 640 } 641 if len(excerpts) > 0 { 642 excerpt = excerpts[3] 643 } 644 if len(locations) > 0 { 645 fn := canonFileName(locations[2]) 646 if *verbose { 647 if s.file != fn { 648 fmt.Printf("%s\n", locations[2]) 649 } 650 fmt.Printf(" %s\n", locations[3]) 651 } 652 s.line = locations[3] 653 s.file = fn 654 s.function = locations[1] 655 addedLine = s.ioState.history.add(s.file, s.line, excerpt) 656 } 657 if len(excerpts) > 0 { 658 if *verbose { 659 fmt.Printf(" %s\n", excerpts[2]) 660 } 661 s.line = excerpts[2] 662 addedLine = s.ioState.history.add(s.file, s.line, excerpt) 663 } 664 665 if !addedLine { 666 // True if this was a repeat line 667 return true 668 } 669 // Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3 670 vars := varsToPrint(excerpt, "//"+s.tag()+"=(") 671 for _, v := range vars { 672 response := printVariableAndNormalize(v, func(v string) string { 673 return s.ioState.writeReadExpect("p "+v+"\n", "[(]gdb[)] ").String() 674 }) 675 s.ioState.history.addVar(response) 676 } 677 return true 678 } 679 680 // printVariableAndNormalize extracts any slash-indicated normalizing requests from the variable 681 // name, then uses printer to get the value of the variable from the debugger, and then 682 // normalizes and returns the response. 683 func printVariableAndNormalize(v string, printer func(v string) string) string { 684 slashIndex := strings.Index(v, "/") 685 substitutions := "" 686 if slashIndex != -1 { 687 substitutions = v[slashIndex:] 688 v = v[:slashIndex] 689 } 690 response := printer(v) 691 // expect something like "$1 = ..." 692 dollar := strings.Index(response, "$") 693 cr := strings.Index(response, "\n") 694 695 if dollar == -1 { // some not entirely expected response, whine and carry on. 696 if cr == -1 { 697 response = strings.TrimSpace(response) // discards trailing newline 698 response = strings.Replace(response, "\n", "<BR>", -1) 699 return "$ Malformed response " + response 700 } 701 response = strings.TrimSpace(response[:cr]) 702 return "$ " + response 703 } 704 if cr == -1 { 705 cr = len(response) 706 } 707 // Convert the leading $<number> into the variable name to enhance readability 708 // and reduce scope of diffs if an earlier print-variable is added. 709 response = strings.TrimSpace(response[dollar:cr]) 710 response = leadingDollarNumberRe.ReplaceAllString(response, v) 711 712 // Normalize value as requested. 713 if strings.Contains(substitutions, "A") { 714 response = hexRe.ReplaceAllString(response, "<A>") 715 } 716 if strings.Contains(substitutions, "N") { 717 response = numRe.ReplaceAllString(response, "<N>") 718 } 719 if strings.Contains(substitutions, "S") { 720 response = stringRe.ReplaceAllString(response, "<S>") 721 } 722 if strings.Contains(substitutions, "O") { 723 response = optOutGdbRe.ReplaceAllString(response, "<Optimized out, as expected>") 724 } 725 return response 726 } 727 728 // varsToPrint takes a source code line, and extracts the comma-separated variable names 729 // found between lookfor and the next ")". 730 // For example, if line includes "... //gdb-foo=(v1,v2,v3)" and 731 // lookfor="//gdb-foo=(", then varsToPrint returns ["v1", "v2", "v3"] 732 func varsToPrint(line, lookfor string) []string { 733 var vars []string 734 if strings.Contains(line, lookfor) { 735 x := line[strings.Index(line, lookfor)+len(lookfor):] 736 end := strings.Index(x, ")") 737 if end == -1 { 738 panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line)) 739 } 740 vars = strings.Split(x[:end], ",") 741 for i, y := range vars { 742 vars[i] = strings.TrimSpace(y) 743 } 744 } 745 return vars 746 } 747 748 func (s *gdbState) quit() { 749 response := s.ioState.writeRead("q\n") 750 if strings.Contains(response.o, "Quit anyway? (y or n)") { 751 defer func() { 752 if r := recover(); r != nil { 753 if s, ok := r.(string); !(ok && strings.Contains(s, "'Y\n'")) { 754 // Not the panic that was expected. 755 fmt.Printf("Expected a broken pipe panic, but saw the following panic instead") 756 panic(r) 757 } 758 } 759 }() 760 s.ioState.writeRead("Y\n") 761 } 762 } 763 764 type ioState struct { 765 stdout io.ReadCloser 766 stderr io.ReadCloser 767 stdin io.WriteCloser 768 outChan chan string 769 errChan chan string 770 last tstring // Output of previous step 771 history *nextHist 772 } 773 774 func newIoState(cmd *exec.Cmd) *ioState { 775 var err error 776 s := &ioState{} 777 s.history = &nextHist{} 778 s.history.f2i = make(map[string]uint8) 779 s.stdout, err = cmd.StdoutPipe() 780 line := asCommandLine("", cmd) 781 if err != nil { 782 panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err)) 783 } 784 s.stderr, err = cmd.StderrPipe() 785 if err != nil { 786 panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err)) 787 } 788 s.stdin, err = cmd.StdinPipe() 789 if err != nil { 790 panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err)) 791 } 792 793 s.outChan = make(chan string, 1) 794 s.errChan = make(chan string, 1) 795 go func() { 796 buffer := make([]byte, 4096) 797 for { 798 n, err := s.stdout.Read(buffer) 799 if n > 0 { 800 s.outChan <- string(buffer[0:n]) 801 } 802 if err == io.EOF || n == 0 { 803 break 804 } 805 if err != nil { 806 fmt.Printf("Saw an error forwarding stdout") 807 break 808 } 809 } 810 close(s.outChan) 811 s.stdout.Close() 812 }() 813 814 go func() { 815 buffer := make([]byte, 4096) 816 for { 817 n, err := s.stderr.Read(buffer) 818 if n > 0 { 819 s.errChan <- string(buffer[0:n]) 820 } 821 if err == io.EOF || n == 0 { 822 break 823 } 824 if err != nil { 825 fmt.Printf("Saw an error forwarding stderr") 826 break 827 } 828 } 829 close(s.errChan) 830 s.stderr.Close() 831 }() 832 return s 833 } 834 835 func (s *ioState) hist() *nextHist { 836 return s.history 837 } 838 839 // writeRead writes ss, then reads stdout and stderr, waiting 500ms to 840 // be sure all the output has appeared. 841 func (s *ioState) writeRead(ss string) tstring { 842 if *verbose { 843 fmt.Printf("=> %s", ss) 844 } 845 _, err := io.WriteString(s.stdin, ss) 846 if err != nil { 847 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 848 } 849 return s.readExpecting(-1, 500, "") 850 } 851 852 // writeReadExpect writes ss, then reads stdout and stderr until something 853 // that matches expectRE appears. expectRE should not be "" 854 func (s *ioState) writeReadExpect(ss, expectRE string) tstring { 855 if *verbose { 856 fmt.Printf("=> %s", ss) 857 } 858 if expectRE == "" { 859 panic("expectRE should not be empty; use .* instead") 860 } 861 _, err := io.WriteString(s.stdin, ss) 862 if err != nil { 863 panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err)) 864 } 865 return s.readSimpleExpecting(expectRE) 866 } 867 868 func (s *ioState) readExpecting(millis, interlineTimeout int, expectedRE string) tstring { 869 timeout := time.Millisecond * time.Duration(millis) 870 interline := time.Millisecond * time.Duration(interlineTimeout) 871 s.last = tstring{} 872 var re *regexp.Regexp 873 if expectedRE != "" { 874 re = regexp.MustCompile(expectedRE) 875 } 876 loop: 877 for { 878 var timer <-chan time.Time 879 if timeout > 0 { 880 timer = time.After(timeout) 881 } 882 select { 883 case x, ok := <-s.outChan: 884 if !ok { 885 s.outChan = nil 886 } 887 s.last.o += x 888 case x, ok := <-s.errChan: 889 if !ok { 890 s.errChan = nil 891 } 892 s.last.e += x 893 case <-timer: 894 break loop 895 } 896 if re != nil { 897 if re.MatchString(s.last.o) { 898 break 899 } 900 if re.MatchString(s.last.e) { 901 break 902 } 903 } 904 timeout = interline 905 } 906 if *verbose { 907 fmt.Printf("<= %s%s", s.last.o, s.last.e) 908 } 909 return s.last 910 } 911 912 func (s *ioState) readSimpleExpecting(expectedRE string) tstring { 913 s.last = tstring{} 914 var re *regexp.Regexp 915 if expectedRE != "" { 916 re = regexp.MustCompile(expectedRE) 917 } 918 for { 919 select { 920 case x, ok := <-s.outChan: 921 if !ok { 922 s.outChan = nil 923 } 924 s.last.o += x 925 case x, ok := <-s.errChan: 926 if !ok { 927 s.errChan = nil 928 } 929 s.last.e += x 930 } 931 if re != nil { 932 if re.MatchString(s.last.o) { 933 break 934 } 935 if re.MatchString(s.last.e) { 936 break 937 } 938 } 939 } 940 if *verbose { 941 fmt.Printf("<= %s%s", s.last.o, s.last.e) 942 } 943 return s.last 944 } 945 946 // replaceEnv returns a new environment derived from env 947 // by removing any existing definition of ev and adding ev=evv. 948 func replaceEnv(env []string, ev string, evv string) []string { 949 if env == nil { 950 env = os.Environ() 951 } 952 evplus := ev + "=" 953 var found bool 954 for i, v := range env { 955 if strings.HasPrefix(v, evplus) { 956 found = true 957 env[i] = evplus + evv 958 } 959 } 960 if !found { 961 env = append(env, evplus+evv) 962 } 963 return env 964 } 965 966 // asCommandLine renders cmd as something that could be copy-and-pasted into a command line 967 // If cwd is not empty and different from the command's directory, prepend an appropriate "cd" 968 func asCommandLine(cwd string, cmd *exec.Cmd) string { 969 s := "(" 970 if cmd.Dir != "" && cmd.Dir != cwd { 971 s += "cd" + escape(cmd.Dir) + ";" 972 } 973 for _, e := range cmd.Env { 974 if !strings.HasPrefix(e, "PATH=") && 975 !strings.HasPrefix(e, "HOME=") && 976 !strings.HasPrefix(e, "USER=") && 977 !strings.HasPrefix(e, "SHELL=") { 978 s += escape(e) 979 } 980 } 981 for _, a := range cmd.Args { 982 s += escape(a) 983 } 984 s += " )" 985 return s 986 } 987 988 // escape inserts escapes appropriate for use in a shell command line 989 func escape(s string) string { 990 s = strings.Replace(s, "\\", "\\\\", -1) 991 s = strings.Replace(s, "'", "\\'", -1) 992 // Conservative guess at characters that will force quoting 993 if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") { 994 s = " '" + s + "'" 995 } else { 996 s = " " + s 997 } 998 return s 999 } 1000 1001 func expect(want string, got tstring) { 1002 if want != "" { 1003 match, err := regexp.MatchString(want, got.o) 1004 if err != nil { 1005 panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err)) 1006 } 1007 if match { 1008 return 1009 } 1010 // Ignore error as we have already checked for it before 1011 match, _ = regexp.MatchString(want, got.e) 1012 if match { 1013 return 1014 } 1015 fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e) 1016 } 1017 }