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