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