mirror of
https://github.com/golang/go.git
synced 2025-11-02 09:40:56 +00:00
787 lines
20 KiB
Go
787 lines
20 KiB
Go
|
|
// Copyright 2017 The Go Authors. All rights reserved.
|
||
|
|
// Use of this source code is governed by a BSD-style
|
||
|
|
// license that can be found in the LICENSE file.
|
||
|
|
|
||
|
|
package ssa_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"flag"
|
||
|
|
"fmt"
|
||
|
|
"internal/testenv"
|
||
|
|
"io"
|
||
|
|
"io/ioutil"
|
||
|
|
"os"
|
||
|
|
"os/exec"
|
||
|
|
"path/filepath"
|
||
|
|
"regexp"
|
||
|
|
"runtime"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
var update = flag.Bool("u", false, "update debug_test reference files")
|
||
|
|
var verbose = flag.Bool("v", false, "print more information about what's happening")
|
||
|
|
var dryrun = flag.Bool("n", false, "just print the command line and first bits")
|
||
|
|
var delve = flag.Bool("d", false, "use delve instead of gdb")
|
||
|
|
var force = flag.Bool("f", false, "force run under not linux-amd64; also do not use tempdir")
|
||
|
|
|
||
|
|
var hexRe = regexp.MustCompile("0x[a-zA-Z0-9]+")
|
||
|
|
var numRe = regexp.MustCompile("-?[0-9]+")
|
||
|
|
var stringRe = regexp.MustCompile("\"([^\\\"]|(\\.))*\"")
|
||
|
|
|
||
|
|
// TestNexting go-builds a file, then uses a debugger (default gdb, optionally delve)
|
||
|
|
// to next through the generated executable, recording each line landed at, and
|
||
|
|
// then compares those lines with reference file(s).
|
||
|
|
// Flag -u updates the reference file(s).
|
||
|
|
// Flag -d changes the debugger to delve (and uses delve-specific reference files)
|
||
|
|
// Flag -v is ever-so-slightly verbose.
|
||
|
|
// Flag -n is for dry-run, and prints the shell and first debug commands.
|
||
|
|
//
|
||
|
|
// The file being tested may contain comments of the form
|
||
|
|
// //DBG-TAG=(v1,v2,v3)
|
||
|
|
// where DBG = {gdb,dlv} and TAG={dbg,opt}
|
||
|
|
// each variable may optionally be followed by a / and one or more of S,A,N
|
||
|
|
// to indicate normalization of Strings, (hex) addresses, and numbers.
|
||
|
|
// For example:
|
||
|
|
/*
|
||
|
|
if len(os.Args) > 1 { //gdb-dbg=(hist/A,cannedInput/A) //dlv-dbg=(hist/A,cannedInput/A)
|
||
|
|
*/
|
||
|
|
// TODO: not implemented for Delve yet, but this is the plan
|
||
|
|
//
|
||
|
|
// After a compiler change that causes a difference in the debug behavior, check
|
||
|
|
// to see if it is sensible or not, and if it is, update the reference files with
|
||
|
|
// go test debug_test.go -args -u
|
||
|
|
// (for Delve)
|
||
|
|
// go test debug_test.go -args -u -d
|
||
|
|
func TestNexting(t *testing.T) {
|
||
|
|
testenv.MustHaveGoBuild(t)
|
||
|
|
testNexting(t, "hist", "dbg", "-N -l")
|
||
|
|
testNexting(t, "hist", "opt", "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func testNexting(t *testing.T, base, tag, gcflags string) {
|
||
|
|
// (1) In testdata, build sample.go into sample
|
||
|
|
// (2) Run debugger gathering a history
|
||
|
|
// (3) Read expected history from testdata/sample.nexts
|
||
|
|
// optionally, write out testdata/sample.nexts
|
||
|
|
|
||
|
|
if !*delve && !*force && !(runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
|
||
|
|
// Running gdb on OSX/darwin is very flaky.
|
||
|
|
// It also probably requires an admin password typed into a dialog box.
|
||
|
|
// Various architectures tend to differ slightly sometimes, and keeping them
|
||
|
|
// all in sync is a pain for people who don't have them all at hand,
|
||
|
|
// so limit testing to amd64 (for now)
|
||
|
|
|
||
|
|
t.Skip()
|
||
|
|
}
|
||
|
|
|
||
|
|
exe := filepath.Join("testdata", base)
|
||
|
|
logbase := exe + "-" + tag
|
||
|
|
tmpbase := logbase + "-test"
|
||
|
|
|
||
|
|
if !*force {
|
||
|
|
tmpdir, err := ioutil.TempDir("", "debug_test")
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("Problem creating TempDir, error %v\n", err))
|
||
|
|
}
|
||
|
|
exe = filepath.Join(tmpdir, base)
|
||
|
|
tmpbase = exe + "-" + tag + "-test"
|
||
|
|
if *verbose {
|
||
|
|
fmt.Printf("Tempdir is %s\n", tmpdir)
|
||
|
|
}
|
||
|
|
defer os.RemoveAll(tmpdir)
|
||
|
|
}
|
||
|
|
|
||
|
|
if gcflags == "" {
|
||
|
|
runGo(t, "", "build", "-o", exe, filepath.Join("testdata", base+".go"))
|
||
|
|
} else {
|
||
|
|
runGo(t, "", "build", "-o", exe, "-gcflags", gcflags, filepath.Join("testdata", base+".go"))
|
||
|
|
}
|
||
|
|
var h1 *nextHist
|
||
|
|
var nextlog, tmplog string
|
||
|
|
if *delve {
|
||
|
|
h1 = dlvTest(tag, exe, 1000)
|
||
|
|
nextlog = logbase + ".delve-nexts"
|
||
|
|
tmplog = tmpbase + ".delve-nexts"
|
||
|
|
} else {
|
||
|
|
h1 = gdbTest(tag, exe, 1000)
|
||
|
|
nextlog = logbase + ".gdb-nexts"
|
||
|
|
tmplog = tmpbase + ".gdb-nexts"
|
||
|
|
}
|
||
|
|
if *dryrun {
|
||
|
|
fmt.Printf("# Tag for above is %s\n", tag)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if *update {
|
||
|
|
h1.write(nextlog)
|
||
|
|
} else {
|
||
|
|
h0 := &nextHist{}
|
||
|
|
h0.read(nextlog)
|
||
|
|
if !h0.equals(h1) {
|
||
|
|
// Be very noisy about exactly what's wrong to simplify debugging.
|
||
|
|
h1.write(tmplog)
|
||
|
|
cmd := exec.Command("diff", "-u", nextlog, tmplog)
|
||
|
|
line := asCommandLine("", cmd)
|
||
|
|
bytes, err := cmd.CombinedOutput()
|
||
|
|
if err != nil && len(bytes) == 0 {
|
||
|
|
t.Fatalf("step/next histories differ, diff command %s failed with error=%v", line, err)
|
||
|
|
}
|
||
|
|
t.Fatalf("step/next histories differ, diff=\n%s", string(bytes))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type dbgr interface {
|
||
|
|
start()
|
||
|
|
do(s string)
|
||
|
|
stepnext(s string) bool // step or next, possible with parameter, gets line etc. returns true for success, false for unsure response
|
||
|
|
quit()
|
||
|
|
hist() *nextHist
|
||
|
|
}
|
||
|
|
|
||
|
|
func gdbTest(tag, executable string, maxNext int, args ...string) *nextHist {
|
||
|
|
dbg := newGdb(tag, executable, args...)
|
||
|
|
dbg.start()
|
||
|
|
if *dryrun {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
for i := 0; i < maxNext; i++ {
|
||
|
|
if !dbg.stepnext("n") {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
h := dbg.hist()
|
||
|
|
return h
|
||
|
|
}
|
||
|
|
|
||
|
|
func dlvTest(tag, executable string, maxNext int, args ...string) *nextHist {
|
||
|
|
dbg := newDelve(tag, executable, args...)
|
||
|
|
dbg.start()
|
||
|
|
if *dryrun {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
for i := 0; i < maxNext; i++ {
|
||
|
|
if !dbg.stepnext("n") {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
h := dbg.hist()
|
||
|
|
return h
|
||
|
|
}
|
||
|
|
|
||
|
|
func runGo(t *testing.T, dir string, args ...string) string {
|
||
|
|
var stdout, stderr bytes.Buffer
|
||
|
|
cmd := exec.Command(testenv.GoToolPath(t), args...)
|
||
|
|
cmd.Dir = dir
|
||
|
|
if *dryrun {
|
||
|
|
fmt.Printf("%s\n", asCommandLine("", cmd))
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
cmd.Stdout = &stdout
|
||
|
|
cmd.Stderr = &stderr
|
||
|
|
|
||
|
|
if err := cmd.Run(); err != nil {
|
||
|
|
t.Fatalf("error running cmd (%s): %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
if s := stderr.String(); s != "" {
|
||
|
|
t.Fatalf("Stderr = %s\nWant empty", s)
|
||
|
|
}
|
||
|
|
|
||
|
|
return stdout.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
type tstring struct {
|
||
|
|
o string
|
||
|
|
e string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (t tstring) String() string {
|
||
|
|
return t.o + t.e
|
||
|
|
}
|
||
|
|
|
||
|
|
type pos struct {
|
||
|
|
line uint16
|
||
|
|
file uint8
|
||
|
|
}
|
||
|
|
|
||
|
|
type nextHist struct {
|
||
|
|
f2i map[string]uint8
|
||
|
|
fs []string
|
||
|
|
ps []pos // TODO: plan to automatically do the minimum distance conversion between a reference and a run for nicer errors.
|
||
|
|
texts []string
|
||
|
|
vars [][]string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *nextHist) write(filename string) {
|
||
|
|
file, err := os.Create(filename)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("Problem opening %s, error %v\n", filename, err))
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
var lastfile uint8
|
||
|
|
for i, x := range h.texts {
|
||
|
|
p := h.ps[i]
|
||
|
|
if lastfile != p.file {
|
||
|
|
fmt.Fprintf(file, " %s\n", h.fs[p.file-1])
|
||
|
|
lastfile = p.file
|
||
|
|
}
|
||
|
|
fmt.Fprintf(file, "%d:%s\n", p.line, x)
|
||
|
|
// Vars must begin with a dollar-sign.
|
||
|
|
// TODO, normalize between gdb and dlv into a common, comparable format.
|
||
|
|
for _, y := range h.vars[i] {
|
||
|
|
y = strings.TrimSpace(y)
|
||
|
|
if y[0] != '$' {
|
||
|
|
panic(fmt.Sprintf("Var line '%s' must begin with $, but does not\n", y))
|
||
|
|
}
|
||
|
|
fmt.Fprintf(file, "%s\n", y)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
file.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *nextHist) read(filename string) {
|
||
|
|
h.f2i = make(map[string]uint8)
|
||
|
|
bytes, err := ioutil.ReadFile(filename)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("Problem reading %s, error %v\n", filename, err))
|
||
|
|
}
|
||
|
|
var lastfile string
|
||
|
|
lines := strings.Split(string(bytes), "\n")
|
||
|
|
for i, l := range lines {
|
||
|
|
if len(l) > 0 && l[0] != '#' {
|
||
|
|
if l[0] == ' ' {
|
||
|
|
// file -- first two characters expected to be " "
|
||
|
|
lastfile = strings.TrimSpace(l)
|
||
|
|
} else if l[0] == '$' {
|
||
|
|
h.addVar(l)
|
||
|
|
} else {
|
||
|
|
// line number -- <number>:<line>
|
||
|
|
colonPos := strings.Index(l, ":")
|
||
|
|
if colonPos == -1 {
|
||
|
|
panic(fmt.Sprintf("Line %d (%s) in file %s expected to contain '<number>:' but does not.\n", i+1, l, filename))
|
||
|
|
}
|
||
|
|
h.add(lastfile, l[0:colonPos], l[colonPos+1:])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *nextHist) add(file, line, text string) {
|
||
|
|
fi := h.f2i[file]
|
||
|
|
if fi == 0 {
|
||
|
|
h.fs = append(h.fs, file)
|
||
|
|
fi = uint8(len(h.fs))
|
||
|
|
h.f2i[file] = fi
|
||
|
|
}
|
||
|
|
|
||
|
|
line = strings.TrimSpace(line)
|
||
|
|
var li int
|
||
|
|
var err error
|
||
|
|
if line != "" {
|
||
|
|
li, err = strconv.Atoi(line)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("Non-numeric line: %s, error %v\n", line, err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
h.ps = append(h.ps, pos{line: uint16(li), file: fi})
|
||
|
|
h.texts = append(h.texts, text)
|
||
|
|
h.vars = append(h.vars, []string{})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *nextHist) addVar(text string) {
|
||
|
|
l := len(h.texts)
|
||
|
|
h.vars[l-1] = append(h.vars[l-1], text)
|
||
|
|
}
|
||
|
|
|
||
|
|
func invertMapSU8(hf2i map[string]uint8) map[uint8]string {
|
||
|
|
hi2f := make(map[uint8]string)
|
||
|
|
for hs, i := range hf2i {
|
||
|
|
hsi := strings.Index(hs, "/src/")
|
||
|
|
if hsi != -1 {
|
||
|
|
hs = hs[hsi+1:]
|
||
|
|
}
|
||
|
|
hi2f[i] = hs
|
||
|
|
}
|
||
|
|
return hi2f
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *nextHist) equals(k *nextHist) bool {
|
||
|
|
if len(h.f2i) != len(k.f2i) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if len(h.ps) != len(k.ps) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
hi2f := invertMapSU8(h.f2i)
|
||
|
|
ki2f := invertMapSU8(k.f2i)
|
||
|
|
|
||
|
|
for i, hs := range hi2f {
|
||
|
|
if hs != ki2f[i] {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for i, x := range h.ps {
|
||
|
|
if k.ps[i] != x {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Delve */
|
||
|
|
|
||
|
|
type delveState struct {
|
||
|
|
cmd *exec.Cmd
|
||
|
|
tag string
|
||
|
|
*ioState
|
||
|
|
atLineRe *regexp.Regexp // "\n =>"
|
||
|
|
funcFileLinePCre *regexp.Regexp // "^> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)"
|
||
|
|
line string
|
||
|
|
file string
|
||
|
|
function string
|
||
|
|
}
|
||
|
|
|
||
|
|
func newDelve(tag, executable string, args ...string) dbgr {
|
||
|
|
cmd := exec.Command("dlv", "exec", executable)
|
||
|
|
cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
|
||
|
|
if len(args) > 0 {
|
||
|
|
cmd.Args = append(cmd.Args, "--")
|
||
|
|
cmd.Args = append(cmd.Args, args...)
|
||
|
|
}
|
||
|
|
s := &delveState{tag: tag, cmd: cmd}
|
||
|
|
// HAHA Delve has control characters embedded to change the color of the => and the line number
|
||
|
|
// that would be '(\\x1b\\[[0-9;]+m)?' OR TERM=dumb
|
||
|
|
s.atLineRe = regexp.MustCompile("\n=>[[:space:]]+[0-9]+:(.*)")
|
||
|
|
s.funcFileLinePCre = regexp.MustCompile("> ([^ ]+) ([^:]+):([0-9]+) .*[(]PC: (0x[a-z0-9]+)[)]\n")
|
||
|
|
s.ioState = newIoState(s.cmd)
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *delveState) stepnext(ss string) bool {
|
||
|
|
x := s.ioState.writeReadExpect(ss+"\n", "[(]dlv[)] ")
|
||
|
|
excerpts := s.atLineRe.FindStringSubmatch(x.o)
|
||
|
|
locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
|
||
|
|
excerpt := ""
|
||
|
|
if len(excerpts) > 1 {
|
||
|
|
excerpt = excerpts[1]
|
||
|
|
}
|
||
|
|
if len(locations) > 0 {
|
||
|
|
if *verbose {
|
||
|
|
if s.file != locations[2] {
|
||
|
|
fmt.Printf("%s\n", locations[2])
|
||
|
|
}
|
||
|
|
fmt.Printf(" %s\n", locations[3])
|
||
|
|
}
|
||
|
|
s.line = locations[3]
|
||
|
|
s.file = locations[2]
|
||
|
|
s.function = locations[1]
|
||
|
|
s.ioState.history.add(s.file, s.line, excerpt)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
fmt.Printf("DID NOT MATCH EXPECTED NEXT OUTPUT\nO='%s'\nE='%s'\n", x.o, x.e)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *delveState) start() {
|
||
|
|
if *dryrun {
|
||
|
|
fmt.Printf("%s\n", asCommandLine("", s.cmd))
|
||
|
|
fmt.Printf("b main.main\n")
|
||
|
|
fmt.Printf("c\n")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
err := s.cmd.Start()
|
||
|
|
if err != nil {
|
||
|
|
line := asCommandLine("", s.cmd)
|
||
|
|
panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
|
||
|
|
}
|
||
|
|
s.ioState.readExpecting(-1, 5000, "Type 'help' for list of commands.")
|
||
|
|
expect("Breakpoint [0-9]+ set at ", s.ioState.writeRead("b main.main\n"))
|
||
|
|
s.stepnext("c")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *delveState) quit() {
|
||
|
|
s.do("q")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *delveState) do(ss string) {
|
||
|
|
expect("", s.ioState.writeRead(ss+"\n"))
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Gdb */
|
||
|
|
|
||
|
|
type gdbState struct {
|
||
|
|
cmd *exec.Cmd
|
||
|
|
tag string
|
||
|
|
args []string
|
||
|
|
*ioState
|
||
|
|
atLineRe *regexp.Regexp
|
||
|
|
funcFileLinePCre *regexp.Regexp
|
||
|
|
line string
|
||
|
|
file string
|
||
|
|
function string
|
||
|
|
}
|
||
|
|
|
||
|
|
func newGdb(tag, executable string, args ...string) dbgr {
|
||
|
|
gdb := "gdb"
|
||
|
|
if runtime.GOOS == "darwin" {
|
||
|
|
gdb = "ggdb" // A possibility on a Mac
|
||
|
|
}
|
||
|
|
cmd := exec.Command(gdb, executable)
|
||
|
|
cmd.Env = replaceEnv(cmd.Env, "TERM", "dumb")
|
||
|
|
s := &gdbState{tag: tag, cmd: cmd, args: args}
|
||
|
|
s.atLineRe = regexp.MustCompile("(^|\n)([0-9]+)(.*)")
|
||
|
|
s.funcFileLinePCre = regexp.MustCompile(
|
||
|
|
"([^ ]+) [(][)][ \\t\\n]+at ([^:]+):([0-9]+)")
|
||
|
|
// runtime.main () at /Users/drchase/GoogleDrive/work/go/src/runtime/proc.go:201
|
||
|
|
// function file line
|
||
|
|
// Thread 2 hit Breakpoint 1, main.main () at /Users/drchase/GoogleDrive/work/debug/hist.go:18
|
||
|
|
s.ioState = newIoState(s.cmd)
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *gdbState) start() {
|
||
|
|
run := "run"
|
||
|
|
for _, a := range s.args {
|
||
|
|
run += " " + a // Can't quote args for gdb, it will pass them through including the quotes
|
||
|
|
}
|
||
|
|
if *dryrun {
|
||
|
|
fmt.Printf("%s\n", asCommandLine("", s.cmd))
|
||
|
|
fmt.Printf("b main.main\n")
|
||
|
|
fmt.Printf("%s\n", run)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
err := s.cmd.Start()
|
||
|
|
if err != nil {
|
||
|
|
line := asCommandLine("", s.cmd)
|
||
|
|
panic(fmt.Sprintf("There was an error [start] running '%s', %v\n", line, err))
|
||
|
|
}
|
||
|
|
s.ioState.readExpecting(-1, 5000, "[(]gdb[)] ")
|
||
|
|
x := s.ioState.writeReadExpect("b main.main\n", "[(]gdb[)] ")
|
||
|
|
expect("Breakpoint [0-9]+ at", x)
|
||
|
|
s.stepnext(run)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *gdbState) stepnext(ss string) bool {
|
||
|
|
x := s.ioState.writeReadExpect(ss+"\n", "[(]gdb[)] ")
|
||
|
|
excerpts := s.atLineRe.FindStringSubmatch(x.o)
|
||
|
|
locations := s.funcFileLinePCre.FindStringSubmatch(x.o)
|
||
|
|
excerpt := ""
|
||
|
|
if len(excerpts) == 0 && len(locations) == 0 {
|
||
|
|
fmt.Printf("DID NOT MATCH %s", x.o)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if len(excerpts) > 0 {
|
||
|
|
excerpt = excerpts[3]
|
||
|
|
}
|
||
|
|
if len(locations) > 0 {
|
||
|
|
if *verbose {
|
||
|
|
if s.file != locations[2] {
|
||
|
|
fmt.Printf("%s\n", locations[2])
|
||
|
|
}
|
||
|
|
fmt.Printf(" %s\n", locations[3])
|
||
|
|
}
|
||
|
|
s.line = locations[3]
|
||
|
|
s.file = locations[2]
|
||
|
|
s.function = locations[1]
|
||
|
|
s.ioState.history.add(s.file, s.line, excerpt)
|
||
|
|
}
|
||
|
|
if len(excerpts) > 0 {
|
||
|
|
if *verbose {
|
||
|
|
fmt.Printf(" %s\n", excerpts[2])
|
||
|
|
}
|
||
|
|
s.line = excerpts[2]
|
||
|
|
s.ioState.history.add(s.file, s.line, excerpt)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Look for //gdb-<tag>=(v1,v2,v3) and print v1, v2, v3
|
||
|
|
vars := varsToPrint(excerpt, "//gdb-"+s.tag+"=(")
|
||
|
|
for _, v := range vars {
|
||
|
|
slashIndex := strings.Index(v, "/")
|
||
|
|
substitutions := ""
|
||
|
|
if slashIndex != -1 {
|
||
|
|
substitutions = v[slashIndex:]
|
||
|
|
v = v[:slashIndex]
|
||
|
|
}
|
||
|
|
response := s.ioState.writeRead("p " + v + "\n").String()
|
||
|
|
// expect something like "$1 = ..."
|
||
|
|
dollar := strings.Index(response, "$")
|
||
|
|
cr := strings.Index(response, "\n")
|
||
|
|
if dollar == -1 {
|
||
|
|
if cr == -1 {
|
||
|
|
response = strings.TrimSpace(response) // discards trailing newline
|
||
|
|
response = strings.Replace(response, "\n", "<BR>", -1)
|
||
|
|
s.ioState.history.addVar("$ Malformed response " + response)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
response = strings.TrimSpace(response[:cr])
|
||
|
|
s.ioState.history.addVar("$ " + response)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if cr == -1 {
|
||
|
|
cr = len(response)
|
||
|
|
}
|
||
|
|
response = strings.TrimSpace(response[dollar:cr])
|
||
|
|
if strings.Contains(substitutions, "A") {
|
||
|
|
response = hexRe.ReplaceAllString(response, "<A>")
|
||
|
|
}
|
||
|
|
if strings.Contains(substitutions, "N") {
|
||
|
|
response = numRe.ReplaceAllString(response, "<N>")
|
||
|
|
}
|
||
|
|
if strings.Contains(substitutions, "S") {
|
||
|
|
response = stringRe.ReplaceAllString(response, "<S>")
|
||
|
|
}
|
||
|
|
s.ioState.history.addVar(response)
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func varsToPrint(line, lookfor string) []string {
|
||
|
|
var vars []string
|
||
|
|
if strings.Contains(line, lookfor) {
|
||
|
|
x := line[strings.Index(line, lookfor)+len(lookfor):]
|
||
|
|
end := strings.Index(x, ")")
|
||
|
|
if end == -1 {
|
||
|
|
panic(fmt.Sprintf("Saw variable list begin %s in %s but no closing ')'", lookfor, line))
|
||
|
|
}
|
||
|
|
vars = strings.Split(x[:end], ",")
|
||
|
|
for i, y := range vars {
|
||
|
|
vars[i] = strings.TrimSpace(y)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return vars
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *gdbState) quit() {
|
||
|
|
response := s.ioState.writeRead("q\n")
|
||
|
|
if strings.Contains(response.o, "Quit anyway? (y or n)") {
|
||
|
|
s.ioState.writeRead("Y\n")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *gdbState) do(ss string) {
|
||
|
|
expect("", s.ioState.writeRead(ss+"\n"))
|
||
|
|
}
|
||
|
|
|
||
|
|
type ioState struct {
|
||
|
|
stdout io.ReadCloser
|
||
|
|
stderr io.ReadCloser
|
||
|
|
stdin io.WriteCloser
|
||
|
|
outChan chan string
|
||
|
|
errChan chan string
|
||
|
|
last tstring // Output of previous step
|
||
|
|
history *nextHist
|
||
|
|
}
|
||
|
|
|
||
|
|
func newIoState(cmd *exec.Cmd) *ioState {
|
||
|
|
var err error
|
||
|
|
s := &ioState{}
|
||
|
|
s.history = &nextHist{}
|
||
|
|
s.history.f2i = make(map[string]uint8)
|
||
|
|
s.stdout, err = cmd.StdoutPipe()
|
||
|
|
line := asCommandLine("", cmd)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("There was an error [stdoutpipe] running '%s', %v\n", line, err))
|
||
|
|
}
|
||
|
|
s.stderr, err = cmd.StderrPipe()
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("There was an error [stdouterr] running '%s', %v\n", line, err))
|
||
|
|
}
|
||
|
|
s.stdin, err = cmd.StdinPipe()
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("There was an error [stdinpipe] running '%s', %v\n", line, err))
|
||
|
|
}
|
||
|
|
|
||
|
|
s.outChan = make(chan string, 1)
|
||
|
|
s.errChan = make(chan string, 1)
|
||
|
|
go func() {
|
||
|
|
buffer := make([]byte, 4096)
|
||
|
|
for {
|
||
|
|
n, err := s.stdout.Read(buffer)
|
||
|
|
if n > 0 {
|
||
|
|
s.outChan <- string(buffer[0:n])
|
||
|
|
}
|
||
|
|
if err == io.EOF || n == 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
fmt.Printf("Saw an error forwarding stdout")
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
close(s.outChan)
|
||
|
|
s.stdout.Close()
|
||
|
|
}()
|
||
|
|
|
||
|
|
go func() {
|
||
|
|
buffer := make([]byte, 4096)
|
||
|
|
for {
|
||
|
|
n, err := s.stderr.Read(buffer)
|
||
|
|
if n > 0 {
|
||
|
|
s.errChan <- string(buffer[0:n])
|
||
|
|
}
|
||
|
|
if err == io.EOF || n == 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
fmt.Printf("Saw an error forwarding stderr")
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
close(s.errChan)
|
||
|
|
s.stderr.Close()
|
||
|
|
}()
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ioState) hist() *nextHist {
|
||
|
|
return s.history
|
||
|
|
}
|
||
|
|
|
||
|
|
const (
|
||
|
|
interlineDelay = 300
|
||
|
|
)
|
||
|
|
|
||
|
|
func (s *ioState) writeRead(ss string) tstring {
|
||
|
|
if *verbose {
|
||
|
|
fmt.Printf("=> %s", ss)
|
||
|
|
}
|
||
|
|
_, err := io.WriteString(s.stdin, ss)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
|
||
|
|
}
|
||
|
|
return s.readWithDelay(-1, interlineDelay)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ioState) writeReadExpect(ss, expect string) tstring {
|
||
|
|
if *verbose {
|
||
|
|
fmt.Printf("=> %s", ss)
|
||
|
|
}
|
||
|
|
_, err := io.WriteString(s.stdin, ss)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("There was an error writing '%s', %v\n", ss, err))
|
||
|
|
}
|
||
|
|
return s.readExpecting(-1, interlineDelay, expect)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ioState) readWithDelay(millis, interlineTimeout int) tstring {
|
||
|
|
return s.readExpecting(millis, interlineTimeout, "")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ioState) readExpecting(millis, interlineTimeout int, expected string) tstring {
|
||
|
|
timeout := time.Millisecond * time.Duration(millis)
|
||
|
|
interline := time.Millisecond * time.Duration(interlineTimeout)
|
||
|
|
s.last = tstring{}
|
||
|
|
var re *regexp.Regexp
|
||
|
|
if expected != "" {
|
||
|
|
re = regexp.MustCompile(expected)
|
||
|
|
}
|
||
|
|
loop:
|
||
|
|
for {
|
||
|
|
var timer <-chan time.Time
|
||
|
|
if timeout > 0 {
|
||
|
|
timer = time.After(timeout)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case x, ok := <-s.outChan:
|
||
|
|
if !ok {
|
||
|
|
s.outChan = nil
|
||
|
|
}
|
||
|
|
s.last.o += x
|
||
|
|
case x, ok := <-s.errChan:
|
||
|
|
if !ok {
|
||
|
|
s.errChan = nil
|
||
|
|
}
|
||
|
|
s.last.e += x
|
||
|
|
case <-timer:
|
||
|
|
break loop
|
||
|
|
}
|
||
|
|
if re != nil {
|
||
|
|
if re.MatchString(s.last.o) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if re.MatchString(s.last.e) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
timeout = interline
|
||
|
|
}
|
||
|
|
if *verbose {
|
||
|
|
fmt.Printf("<= %s%s", s.last.o, s.last.e)
|
||
|
|
}
|
||
|
|
return s.last
|
||
|
|
}
|
||
|
|
|
||
|
|
// replaceEnv returns a new environment derived from env
|
||
|
|
// by removing any existing definition of ev and adding ev=evv.
|
||
|
|
func replaceEnv(env []string, ev string, evv string) []string {
|
||
|
|
evplus := ev + "="
|
||
|
|
var found bool
|
||
|
|
for i, v := range env {
|
||
|
|
if strings.HasPrefix(v, evplus) {
|
||
|
|
found = true
|
||
|
|
env[i] = evplus + evv
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
env = append(env, evplus+evv)
|
||
|
|
}
|
||
|
|
return env
|
||
|
|
}
|
||
|
|
|
||
|
|
// asCommandLine renders cmd as something that could be copy-and-pasted into a command line
|
||
|
|
// If cwd is not empty and different from the command's directory, prepend an approprirate "cd"
|
||
|
|
func asCommandLine(cwd string, cmd *exec.Cmd) string {
|
||
|
|
s := "("
|
||
|
|
if cmd.Dir != "" && cmd.Dir != cwd {
|
||
|
|
s += "cd" + escape(cmd.Dir) + ";"
|
||
|
|
}
|
||
|
|
for _, e := range cmd.Env {
|
||
|
|
if !strings.HasPrefix(e, "PATH=") &&
|
||
|
|
!strings.HasPrefix(e, "HOME=") &&
|
||
|
|
!strings.HasPrefix(e, "USER=") &&
|
||
|
|
!strings.HasPrefix(e, "SHELL=") {
|
||
|
|
s += escape(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, a := range cmd.Args {
|
||
|
|
s += escape(a)
|
||
|
|
}
|
||
|
|
s += " )"
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
// escape inserts escapes appropriate for use in a shell command line
|
||
|
|
func escape(s string) string {
|
||
|
|
s = strings.Replace(s, "\\", "\\\\", -1)
|
||
|
|
s = strings.Replace(s, "'", "\\'", -1)
|
||
|
|
// Conservative guess at characters that will force quoting
|
||
|
|
if strings.ContainsAny(s, "\\ ;#*&$~?!|[]()<>{}`") {
|
||
|
|
s = " '" + s + "'"
|
||
|
|
} else {
|
||
|
|
s = " " + s
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func expect(want string, got tstring) {
|
||
|
|
if want != "" {
|
||
|
|
match, err := regexp.MatchString(want, got.o)
|
||
|
|
if err != nil {
|
||
|
|
panic(fmt.Sprintf("Error for regexp %s, %v\n", want, err))
|
||
|
|
}
|
||
|
|
if match {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
match, err = regexp.MatchString(want, got.e)
|
||
|
|
if match {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
fmt.Printf("EXPECTED '%s'\n GOT O='%s'\nAND E='%s'\n", want, got.o, got.e)
|
||
|
|
}
|
||
|
|
}
|