cmd/dlv: Fix trace output (#2038)

* cmd/dlv,debugger: Improve dlv trace and trace command output

This patch improves the `dlv trace` subcommand output by reducing the
noise that is generated and providing clearer more concise information.

Also adds new tests closing a gap in our testing (we previously never
really tested this subcommand).

This patch also fixes the `dlv trace` REPL command to behave like the
subcommand in certain situations. If the tracepoint is for a function,
we now show function arguements and return values properly.

Also makes the overall output of the trace subcommand clearer.

Fixes #2027
This commit is contained in:
Derek Parker 2020-05-12 23:38:10 -07:00 committed by GitHub
parent a33be4466f
commit f96663a243
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 374 additions and 117 deletions

@ -12,6 +12,9 @@ provided regular expression and output information when tracepoint is hit. This
is useful if you do not want to begin an entire debug session, but merely want is useful if you do not want to begin an entire debug session, but merely want
to know what functions your process is executing. to know what functions your process is executing.
The output of the trace sub command is printed to stderr, so if you would like to
only see the output of the trace operations you can redirect stdout.
``` ```
dlv trace [package] regexp dlv trace [package] regexp
``` ```

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"github.com/go-delve/delve/pkg/config" "github.com/go-delve/delve/pkg/config"
@ -251,7 +252,10 @@ that package instead.`,
The trace sub command will set a tracepoint on every function matching the The trace sub command will set a tracepoint on every function matching the
provided regular expression and output information when tracepoint is hit. This provided regular expression and output information when tracepoint is hit. This
is useful if you do not want to begin an entire debug session, but merely want is useful if you do not want to begin an entire debug session, but merely want
to know what functions your process is executing.`, to know what functions your process is executing.
The output of the trace sub command is printed to stderr, so if you would like to
only see the output of the trace operations you can redirect stdout.`,
Run: traceCmd, Run: traceCmd,
} }
traceCommand.Flags().IntVarP(&traceAttachPid, "pid", "p", 0, "Pid to attach to.") traceCommand.Flags().IntVarP(&traceAttachPid, "pid", "p", 0, "Pid to attach to.")
@ -536,7 +540,7 @@ func traceCmd(cmd *cobra.Command, args []string) {
Stacktrace: traceStackDepth, Stacktrace: traceStackDepth,
LoadArgs: &terminal.ShortLoadConfig, LoadArgs: &terminal.ShortLoadConfig,
}) })
if err != nil { if err != nil && !isBreakpointExistsErr(err) {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
return 1 return 1
} }
@ -549,10 +553,11 @@ func traceCmd(cmd *cobra.Command, args []string) {
_, err = client.CreateBreakpoint(&api.Breakpoint{ _, err = client.CreateBreakpoint(&api.Breakpoint{
Addr: addrs[i], Addr: addrs[i],
TraceReturn: true, TraceReturn: true,
Stacktrace: traceStackDepth,
Line: -1, Line: -1,
LoadArgs: &terminal.ShortLoadConfig, LoadArgs: &terminal.ShortLoadConfig,
}) })
if err != nil { if err != nil && !isBreakpointExistsErr(err) {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
return 1 return 1
} }
@ -561,16 +566,16 @@ func traceCmd(cmd *cobra.Command, args []string) {
cmds := terminal.DebugCommands(client) cmds := terminal.DebugCommands(client)
t := terminal.New(client, nil) t := terminal.New(client, nil)
defer t.Close() defer t.Close()
err = cmds.Call("continue", t) cmds.Call("continue", t)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0 return 0
}() }()
os.Exit(status) os.Exit(status)
} }
func isBreakpointExistsErr(err error) bool {
return strings.Contains(err.Error(), "Breakpoint exists")
}
func testCmd(cmd *cobra.Command, args []string) { func testCmd(cmd *cobra.Command, args []string) {
status := func() int { status := func() int {
debugname, err := filepath.Abs(cmd.Flag("output").Value.String()) debugname, err := filepath.Abs(cmd.Flag("output").Value.String())

@ -23,7 +23,6 @@ import (
"github.com/go-delve/delve/pkg/terminal" "github.com/go-delve/delve/pkg/terminal"
"github.com/go-delve/delve/service/dap/daptest" "github.com/go-delve/delve/service/dap/daptest"
"github.com/go-delve/delve/service/rpc2" "github.com/go-delve/delve/service/rpc2"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
) )
@ -557,3 +556,85 @@ func TestDap(t *testing.T) {
client.Close() client.Close()
cmd.Wait() cmd.Wait()
} }
func TestTrace(t *testing.T) {
dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir)
expected := []byte("> goroutine(1): main.foo(99, 9801) => (9900)\n")
fixtures := protest.FindFixturesDir()
cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "foo")
rdr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
cmd.Dir = filepath.Join(fixtures, "buildtest")
err = cmd.Start()
if err != nil {
t.Fatalf("error running trace: %v", err)
}
output, err := ioutil.ReadAll(rdr)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(output, expected) {
t.Fatalf("expected:\n%s\ngot:\n%s", string(expected), string(output))
}
cmd.Wait()
}
func TestTraceBreakpointExists(t *testing.T) {
dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir)
fixtures := protest.FindFixturesDir()
// We always set breakpoints on some runtime functions at startup, so this would return with
// a breakpoints exists error.
// TODO: Perhaps we shouldn't be setting these default breakpoints in trace mode, however.
cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), filepath.Join(fixtures, "issue573.go"), "runtime.*")
rdr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
cmd.Dir = filepath.Join(fixtures, "buildtest")
err = cmd.Start()
if err != nil {
t.Fatalf("error running trace: %v", err)
}
defer cmd.Wait()
output, err := ioutil.ReadAll(rdr)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(output, []byte("Breakpoint exists")) {
t.Fatal("Breakpoint exists errors should be ignored")
}
}
func TestTracePrintStack(t *testing.T) {
dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir)
fixtures := protest.FindFixturesDir()
cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(tmpdir, "__debug"), "--stack", "2", filepath.Join(fixtures, "issue573.go"), "foo")
rdr, err := cmd.StderrPipe()
if err != nil {
t.Fatal(err)
}
cmd.Dir = filepath.Join(fixtures, "buildtest")
err = cmd.Start()
if err != nil {
t.Fatalf("error running trace: %v", err)
}
defer cmd.Wait()
output, err := ioutil.ReadAll(rdr)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(output, []byte("Stack:")) && !bytes.Contains(output, []byte("main.main")) {
t.Fatal("stacktrace not printed")
}
}

15
pkg/locspec/doc.go Normal file

@ -0,0 +1,15 @@
// Package locspec implements code to parse a string into a specific
// location specification.
//
// Location spec examples:
//
// locStr ::= <filename>:<line> | <function>[:<line>] | /<regex>/ | (+|-)<offset> | <line> | *<address>
// * <filename> can be the full path of a file or just a suffix
// * <function> ::= <package>.<receiver type>.<name> | <package>.(*<receiver type>).<name> | <receiver type>.<name> | <package>.<name> | (*<receiver type>).<name> | <name>
// * <function> must be unambiguous
// * /<regex>/ will return a location for each function matched by regex
// * +<offset> returns a location for the line that is <offset> lines after the current line
// * -<offset> returns a location for the line that is <offset> lines before the current line
// * <line> returns a location for a line in the current file
// * *<address> returns the location corresponding to the specified address
package locspec

@ -1,4 +1,4 @@
package debugger package locspec
import ( import (
"fmt" "fmt"
@ -6,6 +6,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -16,32 +17,44 @@ import (
const maxFindLocationCandidates = 5 const maxFindLocationCandidates = 5
// LocationSpec is an interface that represents a parsed location spec string.
type LocationSpec interface { type LocationSpec interface {
Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) // Find returns all locations that match the location spec.
Find(t *proc.Target, processArgs []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error)
} }
// NormalLocationSpec represents a basic location spec.
// This can be a file:line or func:line.
type NormalLocationSpec struct { type NormalLocationSpec struct {
Base string Base string
FuncBase *FuncLocationSpec FuncBase *FuncLocationSpec
LineOffset int LineOffset int
} }
// RegexLocationSpec represents a regular expression
// location expression such as /^myfunc$/.
type RegexLocationSpec struct { type RegexLocationSpec struct {
FuncRegex string FuncRegex string
} }
// AddrLocationSpec represents an address when used
// as a location spec.
type AddrLocationSpec struct { type AddrLocationSpec struct {
AddrExpr string AddrExpr string
} }
// OffsetLocationSpec represents a location spec that
// is an offset of the current location (file:line).
type OffsetLocationSpec struct { type OffsetLocationSpec struct {
Offset int Offset int
} }
// LineLocationSpec represents a line number in the current file.
type LineLocationSpec struct { type LineLocationSpec struct {
Line int Line int
} }
// FuncLocationSpec represents a function in the target program.
type FuncLocationSpec struct { type FuncLocationSpec struct {
PackageName string PackageName string
AbsolutePackage bool AbsolutePackage bool
@ -50,7 +63,8 @@ type FuncLocationSpec struct {
BaseName string BaseName string
} }
func parseLocationSpec(locStr string) (LocationSpec, error) { // Parse will turn locStr into a parsed LocationSpec.
func Parse(locStr string) (LocationSpec, error) {
rest := locStr rest := locStr
malformed := func(reason string) error { malformed := func(reason string) error {
@ -84,7 +98,7 @@ func parseLocationSpec(locStr string) (LocationSpec, error) {
} }
case '*': case '*':
return &AddrLocationSpec{rest[1:]}, nil return &AddrLocationSpec{AddrExpr: rest[1:]}, nil
default: default:
return parseLocationSpecDefault(locStr, rest) return parseLocationSpecDefault(locStr, rest)
@ -215,6 +229,7 @@ func stripReceiverDecoration(in string) string {
return in[2 : len(in)-1] return in[2 : len(in)-1]
} }
// Match will return whether the provided function matches the location spec.
func (spec *FuncLocationSpec) Match(sym proc.Function, packageMap map[string][]string) bool { func (spec *FuncLocationSpec) Match(sym proc.Function, packageMap map[string][]string) bool {
if spec.BaseName != sym.BaseName() { if spec.BaseName != sym.BaseName() {
return false return false
@ -250,15 +265,17 @@ func packageMatch(specPkg, symPkg string, packageMap map[string][]string) bool {
return partialPackageMatch(specPkg, symPkg) return partialPackageMatch(specPkg, symPkg)
} }
func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { // Find will search all functions in the target program and filter them via the
funcs := d.target.BinInfo().Functions // regex location spec. Only functions matching the regex will be returned.
func (loc *RegexLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
funcs := scope.BinInfo.Functions
matches, err := regexFilterFuncs(loc.FuncRegex, funcs) matches, err := regexFilterFuncs(loc.FuncRegex, funcs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := make([]api.Location, 0, len(matches)) r := make([]api.Location, 0, len(matches))
for i := range matches { for i := range matches {
addrs, _ := proc.FindFunctionLocation(d.target, matches[i], 0) addrs, _ := proc.FindFunctionLocation(t, matches[i], 0)
if len(addrs) > 0 { if len(addrs) > 0 {
r = append(r, addressesToLocation(addrs)) r = append(r, addressesToLocation(addrs))
} }
@ -266,38 +283,40 @@ func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr st
return r, nil return r, nil
} }
func (loc *AddrLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { // Find returns the locations specified via the address location spec.
func (loc *AddrLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
if scope == nil { if scope == nil {
addr, err := strconv.ParseInt(loc.AddrExpr, 0, 64) addr, err := strconv.ParseInt(loc.AddrExpr, 0, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not determine current location (scope is nil)") return nil, fmt.Errorf("could not determine current location (scope is nil)")
} }
return []api.Location{{PC: uint64(addr)}}, nil return []api.Location{{PC: uint64(addr)}}, nil
} else { }
v, err := scope.EvalExpression(loc.AddrExpr, proc.LoadConfig{FollowPointers: true})
v, err := scope.EvalExpression(loc.AddrExpr, proc.LoadConfig{FollowPointers: true})
if err != nil {
return nil, err
}
if v.Unreadable != nil {
return nil, v.Unreadable
}
switch v.Kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
addr, _ := constant.Uint64Val(v.Value)
return []api.Location{{PC: addr}}, nil
case reflect.Func:
fn := scope.BinInfo.PCToFunc(uint64(v.Base))
pc, err := proc.FirstPCAfterPrologue(t, fn, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if v.Unreadable != nil { return []api.Location{{PC: pc}}, nil
return nil, v.Unreadable default:
} return nil, fmt.Errorf("wrong expression kind: %v", v.Kind)
switch v.Kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
addr, _ := constant.Uint64Val(v.Value)
return []api.Location{{PC: addr}}, nil
case reflect.Func:
fn := d.target.BinInfo().PCToFunc(uint64(v.Base))
pc, err := proc.FirstPCAfterPrologue(d.target, fn, false)
if err != nil {
return nil, err
}
return []api.Location{{PC: pc}}, nil
default:
return nil, fmt.Errorf("wrong expression kind: %v", v.Kind)
}
} }
} }
// FileMatch is true if the path matches the location spec.
func (loc *NormalLocationSpec) FileMatch(path string) bool { func (loc *NormalLocationSpec) FileMatch(path string) bool {
return partialPathMatch(loc.Base, path) return partialPathMatch(loc.Base, path)
} }
@ -318,11 +337,12 @@ func partialPathMatch(expr, path string) bool {
func partialPackageMatch(expr, path string) bool { func partialPackageMatch(expr, path string) bool {
if len(expr) < len(path)-1 { if len(expr) < len(path)-1 {
return strings.HasSuffix(path, expr) && (path[len(path)-len(expr)-1] == '/') return strings.HasSuffix(path, expr) && (path[len(path)-len(expr)-1] == '/')
} else {
return expr == path
} }
return expr == path
} }
// AmbiguousLocationError is returned when the location spec
// should only return one location but returns multiple instead.
type AmbiguousLocationError struct { type AmbiguousLocationError struct {
Location string Location string
CandidatesString []string CandidatesString []string
@ -342,11 +362,14 @@ func (ale AmbiguousLocationError) Error() string {
return fmt.Sprintf("Location \"%s\" ambiguous: %s…", ale.Location, strings.Join(candidates, ", ")) return fmt.Sprintf("Location \"%s\" ambiguous: %s…", ale.Location, strings.Join(candidates, ", "))
} }
func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { // Find will return a list of locations that match the given location spec.
// This matches each other location spec that does not already have its own spec
// implemented (such as regex, or addr).
func (loc *NormalLocationSpec) Find(t *proc.Target, processArgs []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
limit := maxFindLocationCandidates limit := maxFindLocationCandidates
var candidateFiles []string var candidateFiles []string
for _, file := range d.target.BinInfo().Sources { for _, file := range scope.BinInfo.Sources {
if loc.FileMatch(file) || (len(d.processArgs) >= 1 && tryMatchRelativePathByProc(loc.Base, d.processArgs[0], file)) { if loc.FileMatch(file) || (len(processArgs) >= 1 && tryMatchRelativePathByProc(loc.Base, processArgs[0], file)) {
candidateFiles = append(candidateFiles, file) candidateFiles = append(candidateFiles, file)
if len(candidateFiles) >= limit { if len(candidateFiles) >= limit {
break break
@ -358,8 +381,8 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
var candidateFuncs []string var candidateFuncs []string
if loc.FuncBase != nil { if loc.FuncBase != nil {
for _, f := range d.target.BinInfo().Functions { for _, f := range scope.BinInfo.Functions {
if !loc.FuncBase.Match(f, d.target.BinInfo().PackageMap) { if !loc.FuncBase.Match(f, scope.BinInfo.PackageMap) {
continue continue
} }
if loc.Base == f.Name { if loc.Base == f.Name {
@ -375,13 +398,13 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
} }
if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 { if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 {
// if no result was found treat this locations string could be an // if no result was found this locations string could be an
// expression that the user forgot to prefix with '*', try treating it as // expression that the user forgot to prefix with '*', try treating it as
// such. // such.
addrSpec := &AddrLocationSpec{locStr} addrSpec := &AddrLocationSpec{AddrExpr: locStr}
locs, err := addrSpec.Find(d, scope, locStr, includeNonExecutableLines) locs, err := addrSpec.Find(t, processArgs, scope, locStr, includeNonExecutableLines)
if err != nil { if err != nil {
return nil, fmt.Errorf("Location \"%s\" not found", locStr) return nil, fmt.Errorf("location \"%s\" not found", locStr)
} }
return locs, nil return locs, nil
} else if matching > 1 { } else if matching > 1 {
@ -395,14 +418,14 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
if loc.LineOffset < 0 { if loc.LineOffset < 0 {
return nil, fmt.Errorf("Malformed breakpoint location, no line offset specified") return nil, fmt.Errorf("Malformed breakpoint location, no line offset specified")
} }
addrs, err = proc.FindFileLocation(d.target, candidateFiles[0], loc.LineOffset) addrs, err = proc.FindFileLocation(t, candidateFiles[0], loc.LineOffset)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, nil return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, nil
} }
} }
} else { // len(candidateFuncs) == 1 } else { // len(candidateFuncs) == 1
addrs, err = proc.FindFunctionLocation(d.target, candidateFuncs[0], loc.LineOffset) addrs, err = proc.FindFunctionLocation(t, candidateFuncs[0], loc.LineOffset)
} }
if err != nil { if err != nil {
@ -418,18 +441,19 @@ func addressesToLocation(addrs []uint64) api.Location {
return api.Location{PC: addrs[0], PCs: addrs} return api.Location{PC: addrs[0], PCs: addrs}
} }
func (loc *OffsetLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { // Find returns the location after adding the offset amount to the current line number.
func (loc *OffsetLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, _ string, includeNonExecutableLines bool) ([]api.Location, error) {
if scope == nil { if scope == nil {
return nil, fmt.Errorf("could not determine current location (scope is nil)") return nil, fmt.Errorf("could not determine current location (scope is nil)")
} }
if loc.Offset == 0 { if loc.Offset == 0 {
return []api.Location{{PC: scope.PC}}, nil return []api.Location{{PC: scope.PC}}, nil
} }
file, line, fn := d.target.BinInfo().PCToLine(scope.PC) file, line, fn := scope.BinInfo.PCToLine(scope.PC)
if fn == nil { if fn == nil {
return nil, fmt.Errorf("could not determine current location") return nil, fmt.Errorf("could not determine current location")
} }
addrs, err := proc.FindFileLocation(d.target, file, line+loc.Offset) addrs, err := proc.FindFileLocation(t, file, line+loc.Offset)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: file, Line: line + loc.Offset}}, nil return []api.Location{{File: file, Line: line + loc.Offset}}, nil
@ -438,15 +462,16 @@ func (loc *OffsetLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
return []api.Location{addressesToLocation(addrs)}, err return []api.Location{addressesToLocation(addrs)}, err
} }
func (loc *LineLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { // Find will return the location at the given line in the current file.
func (loc *LineLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, _ string, includeNonExecutableLines bool) ([]api.Location, error) {
if scope == nil { if scope == nil {
return nil, fmt.Errorf("could not determine current location (scope is nil)") return nil, fmt.Errorf("could not determine current location (scope is nil)")
} }
file, _, fn := d.target.BinInfo().PCToLine(scope.PC) file, _, fn := scope.BinInfo.PCToLine(scope.PC)
if fn == nil { if fn == nil {
return nil, fmt.Errorf("could not determine current location") return nil, fmt.Errorf("could not determine current location")
} }
addrs, err := proc.FindFileLocation(d.target, file, loc.Line) addrs, err := proc.FindFileLocation(t, file, loc.Line)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: file, Line: loc.Line}}, nil return []api.Location{{File: file, Line: loc.Line}}, nil
@ -454,3 +479,18 @@ func (loc *LineLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr str
} }
return []api.Location{addressesToLocation(addrs)}, err return []api.Location{addressesToLocation(addrs)}, err
} }
func regexFilterFuncs(filter string, allFuncs []proc.Function) ([]string, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
funcs := []string{}
for _, f := range allFuncs {
if regex.MatchString(f.Name) {
funcs = append(funcs, f.Name)
}
}
return funcs, nil
}

@ -1,11 +1,11 @@
package debugger package locspec
import ( import (
"testing" "testing"
) )
func parseLocationSpecNoError(t *testing.T, locstr string) LocationSpec { func parseLocationSpecNoError(t *testing.T, locstr string) LocationSpec {
spec, err := parseLocationSpec(locstr) spec, err := Parse(locstr)
if err != nil { if err != nil {
t.Fatalf("Error parsing %q: %v", locstr, err) t.Fatalf("Error parsing %q: %v", locstr, err)
} }

@ -22,9 +22,10 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/cosiner/argv" "github.com/cosiner/argv"
"github.com/go-delve/delve/pkg/locspec"
"github.com/go-delve/delve/service" "github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api" "github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/debugger" "github.com/go-delve/delve/service/rpc2"
) )
const optimizedFunctionWarning = "Warning: debugging optimized function" const optimizedFunctionWarning = "Warning: debugging optimized function"
@ -668,7 +669,7 @@ func printGoroutines(t *Term, gs []*api.Goroutine, fgl formatGoroutineLoc, flags
if err != nil { if err != nil {
return err return err
} }
printStack(stack, "\t", false) printStack(os.Stdout, stack, "\t", false)
} }
} }
return nil return nil
@ -1187,10 +1188,7 @@ func (c *Commands) revCmd(t *Term, ctx callContext, args string) error {
} }
ctx.Prefix = revPrefix ctx.Prefix = revPrefix
if err := c.CallWithContext(args, t, ctx); err != nil { return c.CallWithContext(args, t, ctx)
return err
}
return nil
} }
func (c *Commands) next(t *Term, ctx callContext, args string) error { func (c *Commands) next(t *Term, ctx callContext, args string) error {
@ -1386,31 +1384,31 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) err
args := split2PartsBySpace(argstr) args := split2PartsBySpace(argstr)
requestedBp := &api.Breakpoint{} requestedBp := &api.Breakpoint{}
locspec := "" spec := ""
switch len(args) { switch len(args) {
case 1: case 1:
locspec = argstr spec = argstr
case 2: case 2:
if api.ValidBreakpointName(args[0]) == nil { if api.ValidBreakpointName(args[0]) == nil {
requestedBp.Name = args[0] requestedBp.Name = args[0]
locspec = args[1] spec = args[1]
} else { } else {
locspec = argstr spec = argstr
} }
default: default:
return fmt.Errorf("address required") return fmt.Errorf("address required")
} }
requestedBp.Tracepoint = tracepoint requestedBp.Tracepoint = tracepoint
locs, err := t.client.FindLocation(ctx.Scope, locspec, true) locs, err := t.client.FindLocation(ctx.Scope, spec, true)
if err != nil { if err != nil {
if requestedBp.Name == "" { if requestedBp.Name == "" {
return err return err
} }
requestedBp.Name = "" requestedBp.Name = ""
locspec = argstr spec = argstr
var err2 error var err2 error
locs, err2 = t.client.FindLocation(ctx.Scope, locspec, true) locs, err2 = t.client.FindLocation(ctx.Scope, spec, true)
if err2 != nil { if err2 != nil {
return err return err
} }
@ -1418,6 +1416,9 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) err
for _, loc := range locs { for _, loc := range locs {
requestedBp.Addr = loc.PC requestedBp.Addr = loc.PC
requestedBp.Addrs = loc.PCs requestedBp.Addrs = loc.PCs
if tracepoint {
requestedBp.LoadArgs = &ShortLoadConfig
}
bp, err := t.client.CreateBreakpoint(requestedBp) bp, err := t.client.CreateBreakpoint(requestedBp)
if err != nil { if err != nil {
@ -1426,6 +1427,40 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) err
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp)) fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
} }
var shouldSetReturnBreakpoints bool
loc, err := locspec.Parse(spec)
if err != nil {
return err
}
switch t := loc.(type) {
case *locspec.NormalLocationSpec:
shouldSetReturnBreakpoints = t.LineOffset == -1 && t.FuncBase != nil
case *locspec.RegexLocationSpec:
shouldSetReturnBreakpoints = true
}
if tracepoint && shouldSetReturnBreakpoints && locs[0].Function != nil {
for i := range locs {
if locs[i].Function == nil {
continue
}
addrs, err := t.client.(*rpc2.RPCClient).FunctionReturnLocations(locs[0].Function.Name())
if err != nil {
return err
}
for j := range addrs {
_, err = t.client.CreateBreakpoint(&api.Breakpoint{
Addr: addrs[j],
TraceReturn: true,
Line: -1,
LoadArgs: &ShortLoadConfig,
})
if err != nil {
return err
}
}
}
}
return nil return nil
} }
@ -1528,7 +1563,7 @@ func examineMemoryCmd(t *Term, ctx callContext, args string) error {
return err return err
} }
fmt.Printf(api.PrettyExamineMemory(uintptr(address), memArea, priFmt)) fmt.Print(api.PrettyExamineMemory(uintptr(address), memArea, priFmt))
return nil return nil
} }
@ -1726,7 +1761,7 @@ func stackCommand(t *Term, ctx callContext, args string) error {
if err != nil { if err != nil {
return err return err
} }
printStack(stack, "", sa.offsets) printStack(os.Stdout, stack, "", sa.offsets)
if sa.ancestors > 0 { if sa.ancestors > 0 {
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth) ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
if err != nil { if err != nil {
@ -1738,7 +1773,7 @@ func stackCommand(t *Term, ctx callContext, args string) error {
fmt.Printf("\t%s\n", ancestor.Unreadable) fmt.Printf("\t%s\n", ancestor.Unreadable)
continue continue
} }
printStack(ancestor.Stack, "\t", false) printStack(os.Stdout, ancestor.Stack, "\t", false)
} }
} }
return nil return nil
@ -1872,7 +1907,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
return "", 0, false, err return "", 0, false, err
} }
if len(locs) > 1 { if len(locs) > 1 {
return "", 0, false, debugger.AmbiguousLocationError{Location: args, CandidatesLocation: locs} return "", 0, false, locspec.AmbiguousLocationError{Location: args, CandidatesLocation: locs}
} }
loc := locs[0] loc := locs[0]
if showContext { if showContext {
@ -2000,7 +2035,7 @@ func digits(n int) int {
const stacktraceTruncatedMessage = "(truncated)" const stacktraceTruncatedMessage = "(truncated)"
func printStack(stack []api.Stackframe, ind string, offsets bool) { func printStack(out io.Writer, stack []api.Stackframe, ind string, offsets bool) {
if len(stack) == 0 { if len(stack) == 0 {
return return
} }
@ -2019,42 +2054,42 @@ func printStack(stack []api.Stackframe, ind string, offsets bool) {
for i := range stack { for i := range stack {
if stack[i].Err != "" { if stack[i].Err != "" {
fmt.Printf("%serror: %s\n", s, stack[i].Err) fmt.Fprintf(out, "%serror: %s\n", s, stack[i].Err)
continue continue
} }
fmt.Printf(fmtstr, ind, i, stack[i].PC, stack[i].Function.Name()) fmt.Fprintf(out, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
fmt.Printf("%sat %s:%d\n", s, shortenFilePath(stack[i].File), stack[i].Line) fmt.Fprintf(out, "%sat %s:%d\n", s, shortenFilePath(stack[i].File), stack[i].Line)
if offsets { if offsets {
fmt.Printf("%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset) fmt.Fprintf(out, "%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset)
} }
for j, d := range stack[i].Defers { for j, d := range stack[i].Defers {
deferHeader := fmt.Sprintf("%s defer %d: ", s, j+1) deferHeader := fmt.Sprintf("%s defer %d: ", s, j+1)
s2 := strings.Repeat(" ", len(deferHeader)) s2 := strings.Repeat(" ", len(deferHeader))
if d.Unreadable != "" { if d.Unreadable != "" {
fmt.Printf("%s(unreadable defer: %s)\n", deferHeader, d.Unreadable) fmt.Fprintf(out, "%s(unreadable defer: %s)\n", deferHeader, d.Unreadable)
continue continue
} }
fmt.Printf("%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name()) fmt.Fprintf(out, "%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
fmt.Printf("%sat %s:%d\n", s2, d.DeferredLoc.File, d.DeferredLoc.Line) fmt.Fprintf(out, "%sat %s:%d\n", s2, d.DeferredLoc.File, d.DeferredLoc.Line)
fmt.Printf("%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), d.DeferLoc.File, d.DeferLoc.Line) fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), d.DeferLoc.File, d.DeferLoc.Line)
} }
for j := range stack[i].Arguments { for j := range stack[i].Arguments {
fmt.Printf("%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].SinglelineString()) fmt.Fprintf(out, "%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].SinglelineString())
} }
for j := range stack[i].Locals { for j := range stack[i].Locals {
fmt.Printf("%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].SinglelineString()) fmt.Fprintf(out, "%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].SinglelineString())
} }
if extranl { if extranl {
fmt.Println() fmt.Fprintln(out)
} }
} }
if len(stack) > 0 && !stack[len(stack)-1].Bottom { if len(stack) > 0 && !stack[len(stack)-1].Bottom {
fmt.Printf("%s"+stacktraceTruncatedMessage+"\n", ind) fmt.Fprintf(out, "%s"+stacktraceTruncatedMessage+"\n", ind)
} }
} }
@ -2107,7 +2142,6 @@ func printcontextLocation(loc api.Location) {
if loc.Function != nil && loc.Function.Optimized { if loc.Function != nil && loc.Function.Optimized {
fmt.Println(optimizedFunctionWarning) fmt.Println(optimizedFunctionWarning)
} }
return
} }
func printReturnValues(th *api.Thread) { func printReturnValues(th *api.Thread) {
@ -2131,6 +2165,7 @@ func printcontextThread(t *Term, th *api.Thread) {
} }
args := "" args := ""
var hasReturnValue bool
if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig { if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig {
var arg []string var arg []string
for _, ar := range th.BreakpointInfo.Arguments { for _, ar := range th.BreakpointInfo.Arguments {
@ -2142,6 +2177,9 @@ func printcontextThread(t *Term, th *api.Thread) {
if (ar.Flags & api.VariableArgument) != 0 { if (ar.Flags & api.VariableArgument) != 0 {
arg = append(arg, ar.SinglelineString()) arg = append(arg, ar.SinglelineString())
} }
if (ar.Flags & api.VariableReturnArgument) != 0 {
hasReturnValue = true
}
} }
args = strings.Join(arg, ", ") args = strings.Join(arg, ", ")
} }
@ -2151,6 +2189,11 @@ func printcontextThread(t *Term, th *api.Thread) {
bpname = fmt.Sprintf("[%s] ", th.Breakpoint.Name) bpname = fmt.Sprintf("[%s] ", th.Breakpoint.Name)
} }
if th.Breakpoint.Tracepoint || th.Breakpoint.TraceReturn {
printTracepoint(th, bpname, fn, args, hasReturnValue)
return
}
if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok { if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok {
fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n", fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
bpname, bpname,
@ -2206,7 +2249,29 @@ func printcontextThread(t *Term, th *api.Thread) {
if bpi.Stacktrace != nil { if bpi.Stacktrace != nil {
fmt.Printf("\tStack:\n") fmt.Printf("\tStack:\n")
printStack(bpi.Stacktrace, "\t\t", false) printStack(os.Stdout, bpi.Stacktrace, "\t\t", false)
}
}
}
func printTracepoint(th *api.Thread, bpname string, fn *api.Function, args string, hasReturnValue bool) {
if th.Breakpoint.Tracepoint {
fmt.Fprintf(os.Stderr, "> goroutine(%d): %s%s(%s)", th.GoroutineID, bpname, fn.Name(), args)
if !hasReturnValue {
fmt.Println()
}
}
if th.Breakpoint.TraceReturn {
retVals := make([]string, 0, len(th.ReturnValues))
for _, v := range th.ReturnValues {
retVals = append(retVals, v.SinglelineString())
}
fmt.Fprintf(os.Stderr, " => (%s)\n", strings.Join(retVals, ","))
}
if th.Breakpoint.TraceReturn || !hasReturnValue {
if th.BreakpointInfo.Stacktrace != nil {
fmt.Fprintf(os.Stderr, "\tStack:\n")
printStack(os.Stderr, th.BreakpointInfo.Stacktrace, "\t\t", false)
} }
} }
} }

@ -247,8 +247,8 @@ func TestExecuteFile(t *testing.T) {
} }
func TestIssue354(t *testing.T) { func TestIssue354(t *testing.T) {
printStack([]api.Stackframe{}, "", false) printStack(os.Stdout, []api.Stackframe{}, "", false)
printStack([]api.Stackframe{ printStack(os.Stdout, []api.Stackframe{
{Location: api.Location{PC: 0, File: "irrelevant.go", Line: 10, Function: nil}, {Location: api.Location{PC: 0, File: "irrelevant.go", Line: 10, Function: nil},
Bottom: true}}, "", false) Bottom: true}}, "", false)
} }
@ -263,12 +263,67 @@ func TestIssue411(t *testing.T) {
term.MustExec("trace _fixtures/math.go:9") term.MustExec("trace _fixtures/math.go:9")
term.MustExec("continue") term.MustExec("continue")
out := term.MustExec("next") out := term.MustExec("next")
if !strings.HasPrefix(out, "> main.main()") { if !strings.HasPrefix(out, "> goroutine(1): main.main()") {
t.Fatalf("Wrong output for next: <%s>", out) t.Fatalf("Wrong output for next: <%s>", out)
} }
}) })
} }
func TestTrace(t *testing.T) {
if runtime.GOARCH == "arm64" {
t.Skip("test is not valid on ARM64")
}
test.AllowRecording(t)
withTestTerminal("issue573", t, func(term *FakeTerminal) {
term.MustExec("trace foo")
out, _ := term.Exec("continue")
// The output here is a little strange, but we don't filter stdout vs stderr so it gets jumbled.
// Therefore we assert about the call and return values separately.
if !strings.Contains(out, "> goroutine(1): main.foo(99, 9801)") {
t.Fatalf("Wrong output for tracepoint: %s", out)
}
if !strings.Contains(out, "=> (9900)") {
t.Fatalf("Wrong output for tracepoint return value: %s", out)
}
})
}
func TestTraceWithName(t *testing.T) {
if runtime.GOARCH == "arm64" {
t.Skip("test is not valid on ARM64")
}
test.AllowRecording(t)
withTestTerminal("issue573", t, func(term *FakeTerminal) {
term.MustExec("trace foobar foo")
out, _ := term.Exec("continue")
// The output here is a little strange, but we don't filter stdout vs stderr so it gets jumbled.
// Therefore we assert about the call and return values separately.
if !strings.Contains(out, "> goroutine(1): [foobar] main.foo(99, 9801)") {
t.Fatalf("Wrong output for tracepoint: %s", out)
}
if !strings.Contains(out, "=> (9900)") {
t.Fatalf("Wrong output for tracepoint return value: %s", out)
}
})
}
func TestTraceOnNonFunctionEntry(t *testing.T) {
if runtime.GOARCH == "arm64" {
t.Skip("test is not valid on ARM64")
}
test.AllowRecording(t)
withTestTerminal("issue573", t, func(term *FakeTerminal) {
term.MustExec("trace foobar issue573.go:19")
out, _ := term.Exec("continue")
if !strings.Contains(out, "> goroutine(1): [foobar] main.foo(99, 9801)") {
t.Fatalf("Wrong output for tracepoint: %s", out)
}
if strings.Contains(out, "=> (9900)") {
t.Fatalf("Tracepoint on non-function locspec should not have return value:\n%s", out)
}
})
}
func TestExitStatus(t *testing.T) { func TestExitStatus(t *testing.T) {
withTestTerminal("continuetestprog", t, func(term *FakeTerminal) { withTestTerminal("continuetestprog", t, func(term *FakeTerminal) {
term.Exec("continue") term.Exec("continue")
@ -530,7 +585,7 @@ func TestOnPrefixLocals(t *testing.T) {
}) })
} }
func countOccurrences(s string, needle string) int { func countOccurrences(s, needle string) int {
count := 0 count := 0
for { for {
idx := strings.Index(s, needle) idx := strings.Index(s, needle)

@ -252,8 +252,8 @@ type Variable struct {
Kind reflect.Kind `json:"kind"` Kind reflect.Kind `json:"kind"`
//Strings have their length capped at proc.maxArrayValues, use Len for the real length of a string // Strings have their length capped at proc.maxArrayValues, use Len for the real length of a string
//Function variables will store the name of the function in this field // Function variables will store the name of the function in this field
Value string `json:"value"` Value string `json:"value"`
// Number of elements in an array or a slice, number of keys for a map, number of struct members for a struct, length of strings // Number of elements in an array or a slice, number of keys for a map, number of struct members for a struct, length of strings

@ -36,8 +36,4 @@ type Config struct {
// DisconnectChan will be closed by the server when the client disconnects // DisconnectChan will be closed by the server when the client disconnects
DisconnectChan chan<- struct{} DisconnectChan chan<- struct{}
// TTY is passed along to the target process on creation. Used to specify a
// TTY for that process.
TTY string
} }

@ -17,6 +17,7 @@ import (
"github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/locspec"
"github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/core" "github.com/go-delve/delve/pkg/proc/core"
@ -1069,7 +1070,18 @@ func (d *Debugger) Functions(filter string) ([]string, error) {
d.targetMutex.Lock() d.targetMutex.Lock()
defer d.targetMutex.Unlock() defer d.targetMutex.Unlock()
return regexFilterFuncs(filter, d.target.BinInfo().Functions) regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
funcs := []string{}
for _, f := range d.target.BinInfo().Functions {
if regex.MatchString(f.Name) {
funcs = append(funcs, f.Name)
}
}
return funcs, nil
} }
// Types returns all type information in the binary. // Types returns all type information in the binary.
@ -1097,21 +1109,6 @@ func (d *Debugger) Types(filter string) ([]string, error) {
return r, nil return r, nil
} }
func regexFilterFuncs(filter string, allFuncs []proc.Function) ([]string, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
funcs := []string{}
for _, f := range allFuncs {
if regex.Match([]byte(f.Name)) {
funcs = append(funcs, f.Name)
}
}
return funcs, nil
}
// PackageVariables returns a list of package variables for the thread, // PackageVariables returns a list of package variables for the thread,
// optionally regexp filtered using regexp described in 'filter'. // optionally regexp filtered using regexp described in 'filter'.
func (d *Debugger) PackageVariables(threadID int, filter string, cfg proc.LoadConfig) ([]api.Variable, error) { func (d *Debugger) PackageVariables(threadID int, filter string, cfg proc.LoadConfig) ([]api.Variable, error) {
@ -1443,14 +1440,14 @@ func (d *Debugger) FindLocation(scope api.EvalScope, locStr string, includeNonEx
return nil, err return nil, err
} }
loc, err := parseLocationSpec(locStr) loc, err := locspec.Parse(locStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s, _ := proc.ConvertEvalScope(d.target, scope.GoroutineID, scope.Frame, scope.DeferredCall) s, _ := proc.ConvertEvalScope(d.target, scope.GoroutineID, scope.Frame, scope.DeferredCall)
locs, err := loc.Find(d, s, locStr, includeNonExecutableLines) locs, err := loc.Find(d.target, d.processArgs, s, locStr, includeNonExecutableLines)
for i := range locs { for i := range locs {
if locs[i].PC == 0 { if locs[i].PC == 0 {
continue continue