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