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:
parent
a33be4466f
commit
f96663a243
@ -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
|
||||
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
|
||||
```
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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
|
||||
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
|
||||
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,
|
||||
}
|
||||
traceCommand.Flags().IntVarP(&traceAttachPid, "pid", "p", 0, "Pid to attach to.")
|
||||
@ -536,7 +540,7 @@ func traceCmd(cmd *cobra.Command, args []string) {
|
||||
Stacktrace: traceStackDepth,
|
||||
LoadArgs: &terminal.ShortLoadConfig,
|
||||
})
|
||||
if err != nil {
|
||||
if err != nil && !isBreakpointExistsErr(err) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
@ -549,10 +553,11 @@ func traceCmd(cmd *cobra.Command, args []string) {
|
||||
_, err = client.CreateBreakpoint(&api.Breakpoint{
|
||||
Addr: addrs[i],
|
||||
TraceReturn: true,
|
||||
Stacktrace: traceStackDepth,
|
||||
Line: -1,
|
||||
LoadArgs: &terminal.ShortLoadConfig,
|
||||
})
|
||||
if err != nil {
|
||||
if err != nil && !isBreakpointExistsErr(err) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
@ -561,16 +566,16 @@ func traceCmd(cmd *cobra.Command, args []string) {
|
||||
cmds := terminal.DebugCommands(client)
|
||||
t := terminal.New(client, nil)
|
||||
defer t.Close()
|
||||
err = cmds.Call("continue", t)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
cmds.Call("continue", t)
|
||||
return 0
|
||||
}()
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
func isBreakpointExistsErr(err error) bool {
|
||||
return strings.Contains(err.Error(), "Breakpoint exists")
|
||||
}
|
||||
|
||||
func testCmd(cmd *cobra.Command, args []string) {
|
||||
status := func() int {
|
||||
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/service/dap/daptest"
|
||||
"github.com/go-delve/delve/service/rpc2"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
@ -557,3 +556,85 @@ func TestDap(t *testing.T) {
|
||||
client.Close()
|
||||
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
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 (
|
||||
"fmt"
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -16,32 +17,44 @@ import (
|
||||
|
||||
const maxFindLocationCandidates = 5
|
||||
|
||||
// LocationSpec is an interface that represents a parsed location spec string.
|
||||
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 {
|
||||
Base string
|
||||
FuncBase *FuncLocationSpec
|
||||
LineOffset int
|
||||
}
|
||||
|
||||
// RegexLocationSpec represents a regular expression
|
||||
// location expression such as /^myfunc$/.
|
||||
type RegexLocationSpec struct {
|
||||
FuncRegex string
|
||||
}
|
||||
|
||||
// AddrLocationSpec represents an address when used
|
||||
// as a location spec.
|
||||
type AddrLocationSpec struct {
|
||||
AddrExpr string
|
||||
}
|
||||
|
||||
// OffsetLocationSpec represents a location spec that
|
||||
// is an offset of the current location (file:line).
|
||||
type OffsetLocationSpec struct {
|
||||
Offset int
|
||||
}
|
||||
|
||||
// LineLocationSpec represents a line number in the current file.
|
||||
type LineLocationSpec struct {
|
||||
Line int
|
||||
}
|
||||
|
||||
// FuncLocationSpec represents a function in the target program.
|
||||
type FuncLocationSpec struct {
|
||||
PackageName string
|
||||
AbsolutePackage bool
|
||||
@ -50,7 +63,8 @@ type FuncLocationSpec struct {
|
||||
BaseName string
|
||||
}
|
||||
|
||||
func parseLocationSpec(locStr string) (LocationSpec, error) {
|
||||
// Parse will turn locStr into a parsed LocationSpec.
|
||||
func Parse(locStr string) (LocationSpec, error) {
|
||||
rest := locStr
|
||||
|
||||
malformed := func(reason string) error {
|
||||
@ -84,7 +98,7 @@ func parseLocationSpec(locStr string) (LocationSpec, error) {
|
||||
}
|
||||
|
||||
case '*':
|
||||
return &AddrLocationSpec{rest[1:]}, nil
|
||||
return &AddrLocationSpec{AddrExpr: rest[1:]}, nil
|
||||
|
||||
default:
|
||||
return parseLocationSpecDefault(locStr, rest)
|
||||
@ -215,6 +229,7 @@ func stripReceiverDecoration(in string) string {
|
||||
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 {
|
||||
if spec.BaseName != sym.BaseName() {
|
||||
return false
|
||||
@ -250,15 +265,17 @@ func packageMatch(specPkg, symPkg string, packageMap map[string][]string) bool {
|
||||
return partialPackageMatch(specPkg, symPkg)
|
||||
}
|
||||
|
||||
func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
|
||||
funcs := d.target.BinInfo().Functions
|
||||
// Find will search all functions in the target program and filter them via the
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := make([]api.Location, 0, len(matches))
|
||||
for i := range matches {
|
||||
addrs, _ := proc.FindFunctionLocation(d.target, matches[i], 0)
|
||||
addrs, _ := proc.FindFunctionLocation(t, matches[i], 0)
|
||||
if len(addrs) > 0 {
|
||||
r = append(r, addressesToLocation(addrs))
|
||||
}
|
||||
@ -266,14 +283,16 @@ func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr st
|
||||
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 {
|
||||
addr, err := strconv.ParseInt(loc.AddrExpr, 0, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine current location (scope is nil)")
|
||||
}
|
||||
return []api.Location{{PC: uint64(addr)}}, nil
|
||||
} else {
|
||||
}
|
||||
|
||||
v, err := scope.EvalExpression(loc.AddrExpr, proc.LoadConfig{FollowPointers: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -286,8 +305,8 @@ func (loc *AddrLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr str
|
||||
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)
|
||||
fn := scope.BinInfo.PCToFunc(uint64(v.Base))
|
||||
pc, err := proc.FirstPCAfterPrologue(t, fn, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -295,9 +314,9 @@ func (loc *AddrLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr str
|
||||
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 {
|
||||
return partialPathMatch(loc.Base, path)
|
||||
}
|
||||
@ -318,11 +337,12 @@ func partialPathMatch(expr, path string) bool {
|
||||
func partialPackageMatch(expr, path string) bool {
|
||||
if len(expr) < len(path)-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 {
|
||||
Location string
|
||||
CandidatesString []string
|
||||
@ -342,11 +362,14 @@ func (ale AmbiguousLocationError) Error() string {
|
||||
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
|
||||
var candidateFiles []string
|
||||
for _, file := range d.target.BinInfo().Sources {
|
||||
if loc.FileMatch(file) || (len(d.processArgs) >= 1 && tryMatchRelativePathByProc(loc.Base, d.processArgs[0], file)) {
|
||||
for _, file := range scope.BinInfo.Sources {
|
||||
if loc.FileMatch(file) || (len(processArgs) >= 1 && tryMatchRelativePathByProc(loc.Base, processArgs[0], file)) {
|
||||
candidateFiles = append(candidateFiles, file)
|
||||
if len(candidateFiles) >= limit {
|
||||
break
|
||||
@ -358,8 +381,8 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
|
||||
|
||||
var candidateFuncs []string
|
||||
if loc.FuncBase != nil {
|
||||
for _, f := range d.target.BinInfo().Functions {
|
||||
if !loc.FuncBase.Match(f, d.target.BinInfo().PackageMap) {
|
||||
for _, f := range scope.BinInfo.Functions {
|
||||
if !loc.FuncBase.Match(f, scope.BinInfo.PackageMap) {
|
||||
continue
|
||||
}
|
||||
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 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
|
||||
// such.
|
||||
addrSpec := &AddrLocationSpec{locStr}
|
||||
locs, err := addrSpec.Find(d, scope, locStr, includeNonExecutableLines)
|
||||
addrSpec := &AddrLocationSpec{AddrExpr: locStr}
|
||||
locs, err := addrSpec.Find(t, processArgs, scope, locStr, includeNonExecutableLines)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Location \"%s\" not found", locStr)
|
||||
return nil, fmt.Errorf("location \"%s\" not found", locStr)
|
||||
}
|
||||
return locs, nil
|
||||
} else if matching > 1 {
|
||||
@ -395,14 +418,14 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
|
||||
if loc.LineOffset < 0 {
|
||||
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 _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
|
||||
return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, nil
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
@ -418,18 +441,19 @@ func addressesToLocation(addrs []uint64) api.Location {
|
||||
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 {
|
||||
return nil, fmt.Errorf("could not determine current location (scope is nil)")
|
||||
}
|
||||
if loc.Offset == 0 {
|
||||
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 {
|
||||
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 _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
|
||||
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
|
||||
}
|
||||
|
||||
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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func parseLocationSpecNoError(t *testing.T, locstr string) LocationSpec {
|
||||
spec, err := parseLocationSpec(locstr)
|
||||
spec, err := Parse(locstr)
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing %q: %v", locstr, err)
|
||||
}
|
@ -22,9 +22,10 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/cosiner/argv"
|
||||
"github.com/go-delve/delve/pkg/locspec"
|
||||
"github.com/go-delve/delve/service"
|
||||
"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"
|
||||
@ -668,7 +669,7 @@ func printGoroutines(t *Term, gs []*api.Goroutine, fgl formatGoroutineLoc, flags
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printStack(stack, "\t", false)
|
||||
printStack(os.Stdout, stack, "\t", false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -1187,10 +1188,7 @@ func (c *Commands) revCmd(t *Term, ctx callContext, args string) error {
|
||||
}
|
||||
|
||||
ctx.Prefix = revPrefix
|
||||
if err := c.CallWithContext(args, t, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return c.CallWithContext(args, t, ctx)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
requestedBp := &api.Breakpoint{}
|
||||
locspec := ""
|
||||
spec := ""
|
||||
switch len(args) {
|
||||
case 1:
|
||||
locspec = argstr
|
||||
spec = argstr
|
||||
case 2:
|
||||
if api.ValidBreakpointName(args[0]) == nil {
|
||||
requestedBp.Name = args[0]
|
||||
locspec = args[1]
|
||||
spec = args[1]
|
||||
} else {
|
||||
locspec = argstr
|
||||
spec = argstr
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("address required")
|
||||
}
|
||||
|
||||
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 requestedBp.Name == "" {
|
||||
return err
|
||||
}
|
||||
requestedBp.Name = ""
|
||||
locspec = argstr
|
||||
spec = argstr
|
||||
var err2 error
|
||||
locs, err2 = t.client.FindLocation(ctx.Scope, locspec, true)
|
||||
locs, err2 = t.client.FindLocation(ctx.Scope, spec, true)
|
||||
if err2 != nil {
|
||||
return err
|
||||
}
|
||||
@ -1418,6 +1416,9 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) err
|
||||
for _, loc := range locs {
|
||||
requestedBp.Addr = loc.PC
|
||||
requestedBp.Addrs = loc.PCs
|
||||
if tracepoint {
|
||||
requestedBp.LoadArgs = &ShortLoadConfig
|
||||
}
|
||||
|
||||
bp, err := t.client.CreateBreakpoint(requestedBp)
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -1528,7 +1563,7 @@ func examineMemoryCmd(t *Term, ctx callContext, args string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(api.PrettyExamineMemory(uintptr(address), memArea, priFmt))
|
||||
fmt.Print(api.PrettyExamineMemory(uintptr(address), memArea, priFmt))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1726,7 +1761,7 @@ func stackCommand(t *Term, ctx callContext, args string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printStack(stack, "", sa.offsets)
|
||||
printStack(os.Stdout, stack, "", sa.offsets)
|
||||
if sa.ancestors > 0 {
|
||||
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
|
||||
if err != nil {
|
||||
@ -1738,7 +1773,7 @@ func stackCommand(t *Term, ctx callContext, args string) error {
|
||||
fmt.Printf("\t%s\n", ancestor.Unreadable)
|
||||
continue
|
||||
}
|
||||
printStack(ancestor.Stack, "\t", false)
|
||||
printStack(os.Stdout, ancestor.Stack, "\t", false)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -1872,7 +1907,7 @@ func getLocation(t *Term, ctx callContext, args string, showContext bool) (file
|
||||
return "", 0, false, err
|
||||
}
|
||||
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]
|
||||
if showContext {
|
||||
@ -2000,7 +2035,7 @@ func digits(n int) int {
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -2019,42 +2054,42 @@ func printStack(stack []api.Stackframe, ind string, offsets bool) {
|
||||
|
||||
for i := range stack {
|
||||
if stack[i].Err != "" {
|
||||
fmt.Printf("%serror: %s\n", s, stack[i].Err)
|
||||
fmt.Fprintf(out, "%serror: %s\n", s, stack[i].Err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(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, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
|
||||
fmt.Fprintf(out, "%sat %s:%d\n", s, shortenFilePath(stack[i].File), stack[i].Line)
|
||||
|
||||
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 {
|
||||
deferHeader := fmt.Sprintf("%s defer %d: ", s, j+1)
|
||||
s2 := strings.Repeat(" ", len(deferHeader))
|
||||
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
|
||||
}
|
||||
fmt.Printf("%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.Printf("%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), d.DeferLoc.File, d.DeferLoc.Line)
|
||||
fmt.Fprintf(out, "%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
|
||||
fmt.Fprintf(out, "%sat %s:%d\n", s2, d.DeferredLoc.File, d.DeferredLoc.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 {
|
||||
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 {
|
||||
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 {
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fmt.Println(optimizedFunctionWarning)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func printReturnValues(th *api.Thread) {
|
||||
@ -2131,6 +2165,7 @@ func printcontextThread(t *Term, th *api.Thread) {
|
||||
}
|
||||
|
||||
args := ""
|
||||
var hasReturnValue bool
|
||||
if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig {
|
||||
var arg []string
|
||||
for _, ar := range th.BreakpointInfo.Arguments {
|
||||
@ -2142,6 +2177,9 @@ func printcontextThread(t *Term, th *api.Thread) {
|
||||
if (ar.Flags & api.VariableArgument) != 0 {
|
||||
arg = append(arg, ar.SinglelineString())
|
||||
}
|
||||
if (ar.Flags & api.VariableReturnArgument) != 0 {
|
||||
hasReturnValue = true
|
||||
}
|
||||
}
|
||||
args = strings.Join(arg, ", ")
|
||||
}
|
||||
@ -2151,6 +2189,11 @@ func printcontextThread(t *Term, th *api.Thread) {
|
||||
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 {
|
||||
fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
|
||||
bpname,
|
||||
@ -2206,7 +2249,29 @@ func printcontextThread(t *Term, th *api.Thread) {
|
||||
|
||||
if bpi.Stacktrace != nil {
|
||||
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) {
|
||||
printStack([]api.Stackframe{}, "", false)
|
||||
printStack([]api.Stackframe{
|
||||
printStack(os.Stdout, []api.Stackframe{}, "", false)
|
||||
printStack(os.Stdout, []api.Stackframe{
|
||||
{Location: api.Location{PC: 0, File: "irrelevant.go", Line: 10, Function: nil},
|
||||
Bottom: true}}, "", false)
|
||||
}
|
||||
@ -263,12 +263,67 @@ func TestIssue411(t *testing.T) {
|
||||
term.MustExec("trace _fixtures/math.go:9")
|
||||
term.MustExec("continue")
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
withTestTerminal("continuetestprog", t, func(term *FakeTerminal) {
|
||||
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
|
||||
for {
|
||||
idx := strings.Index(s, needle)
|
||||
|
@ -252,8 +252,8 @@ type Variable struct {
|
||||
|
||||
Kind reflect.Kind `json:"kind"`
|
||||
|
||||
//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
|
||||
// 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
|
||||
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
|
||||
|
@ -36,8 +36,4 @@ type Config struct {
|
||||
|
||||
// DisconnectChan will be closed by the server when the client disconnects
|
||||
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/goversion"
|
||||
"github.com/go-delve/delve/pkg/locspec"
|
||||
"github.com/go-delve/delve/pkg/logflags"
|
||||
"github.com/go-delve/delve/pkg/proc"
|
||||
"github.com/go-delve/delve/pkg/proc/core"
|
||||
@ -1069,7 +1070,18 @@ func (d *Debugger) Functions(filter string) ([]string, error) {
|
||||
d.targetMutex.Lock()
|
||||
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.
|
||||
@ -1097,21 +1109,6 @@ func (d *Debugger) Types(filter string) ([]string, error) {
|
||||
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,
|
||||
// optionally regexp filtered using regexp described in 'filter'.
|
||||
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
|
||||
}
|
||||
|
||||
loc, err := parseLocationSpec(locStr)
|
||||
loc, err := locspec.Parse(locStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
if locs[i].PC == 0 {
|
||||
continue
|
||||
|
Loading…
Reference in New Issue
Block a user