whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PrintGoroutinesFlags uint8
|
||||
|
||||
const (
|
||||
PrintGoroutinesStack PrintGoroutinesFlags = 1 << iota
|
||||
PrintGoroutinesLabels
|
||||
PrintGoroutinesExec
|
||||
)
|
||||
|
||||
type FormatGoroutineLoc int
|
||||
|
||||
const (
|
||||
FglRuntimeCurrent = FormatGoroutineLoc(iota)
|
||||
FglUserCurrent
|
||||
FglGo
|
||||
FglStart
|
||||
)
|
||||
|
||||
const (
|
||||
maxGroupMembers = 5
|
||||
maxGoroutineGroups = 50
|
||||
)
|
||||
|
||||
// The number of goroutines we're going to request on each RPC call
|
||||
const goroutineBatchSize = 10000
|
||||
|
||||
func ParseGoroutineArgs(argstr string) ([]ListGoroutinesFilter, GoroutineGroupingOptions, FormatGoroutineLoc, PrintGoroutinesFlags, int, int, string, error) {
|
||||
args := strings.Split(argstr, " ")
|
||||
var filters []ListGoroutinesFilter
|
||||
var group GoroutineGroupingOptions
|
||||
var fgl = FglUserCurrent
|
||||
var flags PrintGoroutinesFlags
|
||||
var depth = 10
|
||||
var batchSize = goroutineBatchSize
|
||||
var cmd string
|
||||
|
||||
group.MaxGroupMembers = maxGroupMembers
|
||||
group.MaxGroups = maxGoroutineGroups
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch arg {
|
||||
case "-u":
|
||||
fgl = FglUserCurrent
|
||||
case "-r":
|
||||
fgl = FglRuntimeCurrent
|
||||
case "-g":
|
||||
fgl = FglGo
|
||||
case "-s":
|
||||
fgl = FglStart
|
||||
case "-l":
|
||||
flags |= PrintGoroutinesLabels
|
||||
case "-t":
|
||||
flags |= PrintGoroutinesStack
|
||||
// optional depth argument
|
||||
if i+1 < len(args) && len(args[i+1]) > 0 {
|
||||
n, err := strconv.Atoi(args[i+1])
|
||||
if err == nil {
|
||||
depth = n
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
case "-w", "-with":
|
||||
filter, err := readGoroutinesFilter(args, &i)
|
||||
if err != nil {
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
|
||||
}
|
||||
filters = append(filters, *filter)
|
||||
|
||||
case "-wo", "-without":
|
||||
filter, err := readGoroutinesFilter(args, &i)
|
||||
if err != nil {
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
|
||||
}
|
||||
filter.Negated = true
|
||||
filters = append(filters, *filter)
|
||||
|
||||
case "-group":
|
||||
var err error
|
||||
group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
|
||||
if err != nil {
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
|
||||
}
|
||||
i++
|
||||
if group.GroupBy == GoroutineLabel {
|
||||
if i+1 >= len(args) {
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
|
||||
}
|
||||
group.GroupByKey = args[i+1]
|
||||
i++
|
||||
}
|
||||
batchSize = 0 // grouping only works well if run on all goroutines
|
||||
|
||||
case "-chan":
|
||||
i++
|
||||
if i >= len(args) {
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", errors.New("not enough arguments after -chan")
|
||||
}
|
||||
filters = append(filters, ListGoroutinesFilter{Kind: GoroutineWaitingOnChannel, Arg: args[i]})
|
||||
|
||||
case "-exec":
|
||||
flags |= PrintGoroutinesExec
|
||||
cmd = strings.Join(args[i+1:], " ")
|
||||
i = len(args)
|
||||
|
||||
case "":
|
||||
// nothing to do
|
||||
default:
|
||||
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
|
||||
}
|
||||
}
|
||||
return filters, group, fgl, flags, depth, batchSize, cmd, nil
|
||||
}
|
||||
|
||||
func readGoroutinesFilterKind(args []string, i int) (GoroutineField, error) {
|
||||
if i >= len(args) {
|
||||
return GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
|
||||
}
|
||||
|
||||
switch args[i] {
|
||||
case "curloc":
|
||||
return GoroutineCurrentLoc, nil
|
||||
case "userloc":
|
||||
return GoroutineUserLoc, nil
|
||||
case "goloc":
|
||||
return GoroutineGoLoc, nil
|
||||
case "startloc":
|
||||
return GoroutineStartLoc, nil
|
||||
case "label":
|
||||
return GoroutineLabel, nil
|
||||
case "running":
|
||||
return GoroutineRunning, nil
|
||||
case "user":
|
||||
return GoroutineUser, nil
|
||||
default:
|
||||
return GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
|
||||
}
|
||||
}
|
||||
|
||||
func readGoroutinesFilter(args []string, pi *int) (*ListGoroutinesFilter, error) {
|
||||
r := new(ListGoroutinesFilter)
|
||||
var err error
|
||||
r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*pi++
|
||||
switch r.Kind {
|
||||
case GoroutineRunning, GoroutineUser:
|
||||
return r, nil
|
||||
}
|
||||
if *pi+1 >= len(args) {
|
||||
return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
|
||||
}
|
||||
r.Arg = args[*pi+1]
|
||||
*pi++
|
||||
|
||||
return r, nil
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/constant"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-delve/delve/pkg/dwarf/godwarf"
|
||||
"github.com/go-delve/delve/pkg/dwarf/op"
|
||||
"github.com/go-delve/delve/pkg/proc"
|
||||
)
|
||||
|
||||
// ConvertLogicalBreakpoint converts a proc.LogicalBreakpoint into an API breakpoint.
|
||||
func ConvertLogicalBreakpoint(lbp *proc.LogicalBreakpoint) *Breakpoint {
|
||||
b := &Breakpoint{
|
||||
ID: lbp.LogicalID,
|
||||
FunctionName: lbp.FunctionName,
|
||||
File: lbp.File,
|
||||
Line: lbp.Line,
|
||||
Name: lbp.Name,
|
||||
Tracepoint: lbp.Tracepoint,
|
||||
TraceReturn: lbp.TraceReturn,
|
||||
Stacktrace: lbp.Stacktrace,
|
||||
Goroutine: lbp.Goroutine,
|
||||
Variables: lbp.Variables,
|
||||
LoadArgs: LoadConfigFromProc(lbp.LoadArgs),
|
||||
LoadLocals: LoadConfigFromProc(lbp.LoadLocals),
|
||||
TotalHitCount: lbp.TotalHitCount,
|
||||
Disabled: !lbp.Enabled,
|
||||
UserData: lbp.UserData,
|
||||
RootFuncName: lbp.RootFuncName,
|
||||
TraceFollowCalls: lbp.TraceFollowCalls,
|
||||
}
|
||||
|
||||
b.HitCount = map[string]uint64{}
|
||||
for idx := range lbp.HitCount {
|
||||
b.HitCount[strconv.FormatInt(idx, 10)] = lbp.HitCount[idx]
|
||||
}
|
||||
|
||||
if lbp.HitCond != nil {
|
||||
b.HitCond = fmt.Sprintf("%s %d", lbp.HitCond.Op.String(), lbp.HitCond.Val)
|
||||
b.HitCondPerG = lbp.HitCondPerG
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
printer.Fprint(&buf, token.NewFileSet(), lbp.Cond)
|
||||
b.Cond = buf.String()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// ConvertPhysicalBreakpoints adds information from physical breakpoints to an API breakpoint.
|
||||
func ConvertPhysicalBreakpoints(b *Breakpoint, lbp *proc.LogicalBreakpoint, pids []int, bps []*proc.Breakpoint) {
|
||||
if len(bps) == 0 {
|
||||
if lbp != nil {
|
||||
b.ExprString = lbp.Set.ExprString
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.WatchExpr = bps[0].WatchExpr
|
||||
b.WatchType = WatchType(bps[0].WatchType)
|
||||
|
||||
lg := false
|
||||
for i, bp := range bps {
|
||||
b.Addrs = append(b.Addrs, bp.Addr)
|
||||
b.AddrPid = append(b.AddrPid, pids[i])
|
||||
if b.FunctionName != bp.FunctionName && b.FunctionName != "" {
|
||||
if !lg {
|
||||
b.FunctionName = removeTypeParams(b.FunctionName)
|
||||
lg = true
|
||||
}
|
||||
fn := removeTypeParams(bp.FunctionName)
|
||||
if b.FunctionName != fn {
|
||||
b.FunctionName = "(multiple functions)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.Addrs) > 0 {
|
||||
b.Addr = b.Addrs[0]
|
||||
}
|
||||
}
|
||||
|
||||
func removeTypeParams(name string) string {
|
||||
fn := proc.Function{Name: name}
|
||||
return fn.NameWithoutTypeParams()
|
||||
}
|
||||
|
||||
// ConvertThread converts a proc.Thread into an
|
||||
// api thread.
|
||||
func ConvertThread(th proc.Thread, bp *Breakpoint) *Thread {
|
||||
var (
|
||||
function *Function
|
||||
file string
|
||||
line int
|
||||
pc uint64
|
||||
gid int64
|
||||
)
|
||||
|
||||
loc, err := th.Location()
|
||||
if err == nil {
|
||||
pc = loc.PC
|
||||
file = loc.File
|
||||
line = loc.Line
|
||||
function = ConvertFunction(loc.Fn)
|
||||
}
|
||||
|
||||
if g, _ := proc.GetG(th); g != nil {
|
||||
gid = g.ID
|
||||
}
|
||||
|
||||
return &Thread{
|
||||
ID: th.ThreadID(),
|
||||
PC: pc,
|
||||
File: file,
|
||||
Line: line,
|
||||
Function: function,
|
||||
GoroutineID: gid,
|
||||
Breakpoint: bp,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertThreads converts a slice of proc.Thread into a slice of api.Thread.
|
||||
func ConvertThreads(threads []proc.Thread, convertBreakpoint func(proc.Thread) *Breakpoint) []*Thread {
|
||||
r := make([]*Thread, len(threads))
|
||||
for i := range threads {
|
||||
r[i] = ConvertThread(threads[i], convertBreakpoint(threads[i]))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func PrettyTypeName(typ godwarf.Type) string {
|
||||
if typ == nil {
|
||||
return ""
|
||||
}
|
||||
if typ.Common().Name != "" {
|
||||
return typ.Common().Name
|
||||
}
|
||||
r := typ.String()
|
||||
if r == "*void" {
|
||||
return "unsafe.Pointer"
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func convertFloatValue(v *proc.Variable, sz int) string {
|
||||
switch v.FloatSpecial {
|
||||
case proc.FloatIsPosInf:
|
||||
return "+Inf"
|
||||
case proc.FloatIsNegInf:
|
||||
return "-Inf"
|
||||
case proc.FloatIsNaN:
|
||||
return "NaN"
|
||||
}
|
||||
f, _ := constant.Float64Val(v.Value)
|
||||
return strconv.FormatFloat(f, 'f', -1, sz)
|
||||
}
|
||||
|
||||
// ConvertVar converts from proc.Variable to api.Variable.
|
||||
func ConvertVar(v *proc.Variable) *Variable {
|
||||
r := Variable{
|
||||
Addr: v.Addr,
|
||||
OnlyAddr: v.OnlyAddr,
|
||||
Name: v.Name,
|
||||
Kind: v.Kind,
|
||||
Len: v.Len,
|
||||
Cap: v.Cap,
|
||||
Flags: VariableFlags(v.Flags),
|
||||
Base: v.Base,
|
||||
|
||||
LocationExpr: v.LocationExpr.String(),
|
||||
DeclLine: v.DeclLine,
|
||||
}
|
||||
|
||||
r.Type = PrettyTypeName(v.DwarfType)
|
||||
r.RealType = PrettyTypeName(v.RealType)
|
||||
|
||||
if v.Unreadable != nil {
|
||||
r.Unreadable = v.Unreadable.Error()
|
||||
}
|
||||
|
||||
r.Value = VariableValueAsString(v)
|
||||
|
||||
switch v.Kind {
|
||||
case reflect.Complex64:
|
||||
r.Children = make([]Variable, 2)
|
||||
r.Len = 2
|
||||
|
||||
r.Children[0].Name = "real"
|
||||
r.Children[0].Kind = reflect.Float32
|
||||
|
||||
r.Children[1].Name = "imaginary"
|
||||
r.Children[1].Kind = reflect.Float32
|
||||
|
||||
if v.Value != nil {
|
||||
real, _ := constant.Float64Val(constant.Real(v.Value))
|
||||
r.Children[0].Value = strconv.FormatFloat(real, 'f', -1, 32)
|
||||
|
||||
imag, _ := constant.Float64Val(constant.Imag(v.Value))
|
||||
r.Children[1].Value = strconv.FormatFloat(imag, 'f', -1, 32)
|
||||
} else {
|
||||
r.Children[0].Value = "nil"
|
||||
r.Children[1].Value = "nil"
|
||||
}
|
||||
|
||||
case reflect.Complex128:
|
||||
r.Children = make([]Variable, 2)
|
||||
r.Len = 2
|
||||
|
||||
r.Children[0].Name = "real"
|
||||
r.Children[0].Kind = reflect.Float64
|
||||
|
||||
r.Children[1].Name = "imaginary"
|
||||
r.Children[1].Kind = reflect.Float64
|
||||
|
||||
if v.Value != nil {
|
||||
real, _ := constant.Float64Val(constant.Real(v.Value))
|
||||
r.Children[0].Value = strconv.FormatFloat(real, 'f', -1, 64)
|
||||
|
||||
imag, _ := constant.Float64Val(constant.Imag(v.Value))
|
||||
r.Children[1].Value = strconv.FormatFloat(imag, 'f', -1, 64)
|
||||
} else {
|
||||
r.Children[0].Value = "nil"
|
||||
r.Children[1].Value = "nil"
|
||||
}
|
||||
|
||||
default:
|
||||
r.Children = make([]Variable, len(v.Children))
|
||||
|
||||
for i := range v.Children {
|
||||
r.Children[i] = *ConvertVar(&v.Children[i])
|
||||
}
|
||||
}
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
func VariableValueAsString(v *proc.Variable) string {
|
||||
if v.Value == nil {
|
||||
return ""
|
||||
}
|
||||
switch v.Kind {
|
||||
case reflect.Float32:
|
||||
return convertFloatValue(v, 32)
|
||||
case reflect.Float64:
|
||||
return convertFloatValue(v, 64)
|
||||
case reflect.String, reflect.Func, reflect.Struct:
|
||||
return constant.StringVal(v.Value)
|
||||
default:
|
||||
if cd := v.ConstDescr(); cd != "" {
|
||||
return fmt.Sprintf("%s (%s)", cd, v.Value.String())
|
||||
} else {
|
||||
return v.Value.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertVars converts from []*proc.Variable to []api.Variable.
|
||||
func ConvertVars(pv []*proc.Variable) []Variable {
|
||||
if pv == nil {
|
||||
return nil
|
||||
}
|
||||
vars := make([]Variable, 0, len(pv))
|
||||
for _, v := range pv {
|
||||
vars = append(vars, *ConvertVar(v))
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
// ConvertFunction converts from gosym.Func to
|
||||
// api.Function.
|
||||
func ConvertFunction(fn *proc.Function) *Function {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fn here used to be a *gosym.Func, the fields Type and GoType below
|
||||
// corresponded to the homonymous field of gosym.Func. Since the contents of
|
||||
// those fields is not documented their value was replaced with 0 when
|
||||
// gosym.Func was replaced by debug_info entries.
|
||||
return &Function{
|
||||
Name_: fn.Name,
|
||||
Type: 0,
|
||||
Value: fn.Entry,
|
||||
GoType: 0,
|
||||
Optimized: fn.Optimized(),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertGoroutine converts from proc.G to api.Goroutine.
|
||||
func ConvertGoroutine(tgt *proc.Target, g *proc.G) *Goroutine {
|
||||
th := g.Thread
|
||||
tid := 0
|
||||
if th != nil {
|
||||
tid = th.ThreadID()
|
||||
}
|
||||
if g.Unreadable != nil {
|
||||
return &Goroutine{Unreadable: g.Unreadable.Error()}
|
||||
}
|
||||
return &Goroutine{
|
||||
ID: g.ID,
|
||||
CurrentLoc: ConvertLocation(g.CurrentLoc),
|
||||
UserCurrentLoc: ConvertLocation(g.UserCurrent()),
|
||||
GoStatementLoc: ConvertLocation(g.Go()),
|
||||
StartLoc: ConvertLocation(g.StartLoc(tgt)),
|
||||
ThreadID: tid,
|
||||
WaitSince: g.WaitSince,
|
||||
WaitReason: g.WaitReason,
|
||||
Labels: g.Labels(),
|
||||
Status: g.Status,
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertGoroutines converts from []*proc.G to []*api.Goroutine.
|
||||
func ConvertGoroutines(tgt *proc.Target, gs []*proc.G) []*Goroutine {
|
||||
goroutines := make([]*Goroutine, len(gs))
|
||||
for i := range gs {
|
||||
goroutines[i] = ConvertGoroutine(tgt, gs[i])
|
||||
}
|
||||
return goroutines
|
||||
}
|
||||
|
||||
// ConvertLocation converts from proc.Location to api.Location.
|
||||
func ConvertLocation(loc proc.Location) Location {
|
||||
return Location{
|
||||
PC: loc.PC,
|
||||
File: loc.File,
|
||||
Line: loc.Line,
|
||||
Function: ConvertFunction(loc.Fn),
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertAsmInstruction converts from proc.AsmInstruction to api.AsmInstruction.
|
||||
func ConvertAsmInstruction(inst proc.AsmInstruction, text string) AsmInstruction {
|
||||
var destloc *Location
|
||||
if inst.DestLoc != nil {
|
||||
r := ConvertLocation(*inst.DestLoc)
|
||||
destloc = &r
|
||||
}
|
||||
return AsmInstruction{
|
||||
Loc: ConvertLocation(inst.Loc),
|
||||
DestLoc: destloc,
|
||||
Text: text,
|
||||
Bytes: inst.Bytes,
|
||||
Breakpoint: inst.Breakpoint,
|
||||
AtPC: inst.AtPC,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfigToProc converts an api.LoadConfig to proc.LoadConfig.
|
||||
func LoadConfigToProc(cfg *LoadConfig) *proc.LoadConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return &proc.LoadConfig{
|
||||
FollowPointers: cfg.FollowPointers,
|
||||
MaxVariableRecurse: cfg.MaxVariableRecurse,
|
||||
MaxStringLen: cfg.MaxStringLen,
|
||||
MaxArrayValues: cfg.MaxArrayValues,
|
||||
MaxStructFields: cfg.MaxStructFields,
|
||||
MaxMapBuckets: 0, // MaxMapBuckets is set internally by pkg/proc, read its documentation for an explanation.
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfigFromProc converts a proc.LoadConfig to api.LoadConfig.
|
||||
func LoadConfigFromProc(cfg *proc.LoadConfig) *LoadConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
return &LoadConfig{
|
||||
FollowPointers: cfg.FollowPointers,
|
||||
MaxVariableRecurse: cfg.MaxVariableRecurse,
|
||||
MaxStringLen: cfg.MaxStringLen,
|
||||
MaxArrayValues: cfg.MaxArrayValues,
|
||||
MaxStructFields: cfg.MaxStructFields,
|
||||
}
|
||||
}
|
||||
|
||||
var canonicalRegisterOrder = map[string]int{
|
||||
// amd64
|
||||
"rip": 0,
|
||||
"rsp": 1,
|
||||
"rax": 2,
|
||||
"rbx": 3,
|
||||
"rcx": 4,
|
||||
"rdx": 5,
|
||||
|
||||
// arm64
|
||||
"pc": 0,
|
||||
"sp": 1,
|
||||
}
|
||||
|
||||
// ConvertRegisters converts proc.Register to api.Register for a slice.
|
||||
func ConvertRegisters(in *op.DwarfRegisters, dwarfRegisterToString func(int, *op.DwarfRegister) (string, bool, string), floatingPoint bool) (out []Register) {
|
||||
out = make([]Register, 0, in.CurrentSize())
|
||||
for i := 0; i < in.CurrentSize(); i++ {
|
||||
reg := in.Reg(uint64(i))
|
||||
if reg == nil {
|
||||
continue
|
||||
}
|
||||
name, fp, repr := dwarfRegisterToString(i, reg)
|
||||
if !floatingPoint && fp {
|
||||
continue
|
||||
}
|
||||
out = append(out, Register{name, repr, i})
|
||||
}
|
||||
// Sort the registers in a canonical order we prefer, this is mostly
|
||||
// because the DWARF register numbering for AMD64 is weird.
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
a, b := out[i], out[j]
|
||||
an, aok := canonicalRegisterOrder[strings.ToLower(a.Name)]
|
||||
bn, bok := canonicalRegisterOrder[strings.ToLower(b.Name)]
|
||||
// Registers that don't appear in canonicalRegisterOrder sort after registers that do.
|
||||
if !aok {
|
||||
an = 1000
|
||||
}
|
||||
if !bok {
|
||||
bn = 1000
|
||||
}
|
||||
if an == bn {
|
||||
// keep registers that don't appear in canonicalRegisterOrder in DWARF order
|
||||
return a.DwarfNumber < b.DwarfNumber
|
||||
}
|
||||
return an < bn
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConvertImage converts proc.Image to api.Image.
|
||||
func ConvertImage(image *proc.Image) Image {
|
||||
err := image.LoadError()
|
||||
lerr := ""
|
||||
if err != nil {
|
||||
lerr = err.Error()
|
||||
}
|
||||
return Image{Path: image.Path, Address: image.StaticBase, LoadError: lerr}
|
||||
}
|
||||
|
||||
// ConvertDumpState converts proc.DumpState to api.DumpState.
|
||||
func ConvertDumpState(dumpState *proc.DumpState) *DumpState {
|
||||
dumpState.Mutex.Lock()
|
||||
defer dumpState.Mutex.Unlock()
|
||||
r := &DumpState{
|
||||
Dumping: dumpState.Dumping,
|
||||
AllDone: dumpState.AllDone,
|
||||
ThreadsDone: dumpState.ThreadsDone,
|
||||
ThreadsTotal: dumpState.ThreadsTotal,
|
||||
MemDone: dumpState.MemDone,
|
||||
MemTotal: dumpState.MemTotal,
|
||||
}
|
||||
if dumpState.Err != nil {
|
||||
r.Err = dumpState.Err.Error()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ConvertTarget converts a proc.Target into a api.Target.
|
||||
func ConvertTarget(tgt *proc.Target, convertThreadBreakpoint func(proc.Thread) *Breakpoint) *Target {
|
||||
return &Target{
|
||||
Pid: tgt.Pid(),
|
||||
CmdLine: tgt.CmdLine,
|
||||
CurrentThread: ConvertThread(tgt.CurrentThread(), convertThreadBreakpoint(tgt.CurrentThread())),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
const (
|
||||
// strings longer than this will cause slices, arrays and structs to be printed on multiple lines when newlines is enabled
|
||||
maxShortStringLen = 7
|
||||
// string used for one indentation level (when printing on multiple lines)
|
||||
indentString = "\t"
|
||||
)
|
||||
|
||||
type prettyFlags uint8
|
||||
|
||||
const (
|
||||
prettyTop prettyFlags = 1 << iota
|
||||
prettyNewlines
|
||||
prettyIncludeType
|
||||
prettyShortenType
|
||||
)
|
||||
|
||||
func (flags prettyFlags) top() bool { return flags&prettyTop != 0 }
|
||||
func (flags prettyFlags) includeType() bool { return flags&prettyIncludeType != 0 }
|
||||
func (flags prettyFlags) newlines() bool { return flags&prettyNewlines != 0 }
|
||||
func (flags prettyFlags) shortenType() bool { return flags&prettyShortenType != 0 }
|
||||
|
||||
func (flags prettyFlags) set(flag prettyFlags, v bool) prettyFlags {
|
||||
if v {
|
||||
return flags | flag
|
||||
} else {
|
||||
return flags &^ flag
|
||||
}
|
||||
}
|
||||
|
||||
// SinglelineString returns a representation of v on a single line.
|
||||
func (v *Variable) SinglelineString() string {
|
||||
var buf bytes.Buffer
|
||||
v.writeTo(&buf, prettyTop|prettyIncludeType, "", "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SinglelineStringWithShortTypes returns a representation of v on a single line, with types shortened.
|
||||
func (v *Variable) SinglelineStringWithShortTypes() string {
|
||||
var buf bytes.Buffer
|
||||
v.writeTo(&buf, prettyTop|prettyIncludeType|prettyShortenType, "", "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SinglelineStringFormatted returns a representation of v on a single line, using the format specified by fmtstr.
|
||||
func (v *Variable) SinglelineStringFormatted(fmtstr string) string {
|
||||
var buf bytes.Buffer
|
||||
v.writeTo(&buf, prettyTop|prettyIncludeType, "", fmtstr)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// MultilineString returns a representation of v on multiple lines.
|
||||
func (v *Variable) MultilineString(indent, fmtstr string) string {
|
||||
var buf bytes.Buffer
|
||||
v.writeTo(&buf, prettyTop|prettyNewlines|prettyIncludeType, indent, fmtstr)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (v *Variable) typeStr(flags prettyFlags) string {
|
||||
if flags.shortenType() {
|
||||
return ShortenType(v.Type)
|
||||
}
|
||||
|
||||
return v.Type
|
||||
}
|
||||
|
||||
func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if v.Unreadable != "" {
|
||||
fmt.Fprintf(buf, "(unreadable %s)", v.Unreadable)
|
||||
return
|
||||
}
|
||||
|
||||
if !flags.top() && v.Addr == 0 && v.Value == "" {
|
||||
if flags.includeType() && v.Type != "void" {
|
||||
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
|
||||
} else {
|
||||
fmt.Fprint(buf, "nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch v.Kind {
|
||||
case reflect.Slice:
|
||||
v.writeSliceTo(buf, flags, indent, fmtstr)
|
||||
case reflect.Array:
|
||||
v.writeArrayTo(buf, flags, indent, fmtstr)
|
||||
case reflect.Ptr:
|
||||
if v.Type == "" || len(v.Children) == 0 {
|
||||
fmt.Fprint(buf, "nil")
|
||||
} else if v.Children[0].OnlyAddr && v.Children[0].Addr != 0 {
|
||||
v.writePointerTo(buf, flags)
|
||||
} else {
|
||||
if flags.top() && flags.newlines() && v.Children[0].Addr != 0 {
|
||||
v.writePointerTo(buf, flags)
|
||||
fmt.Fprint(buf, "\n")
|
||||
}
|
||||
fmt.Fprint(buf, "*")
|
||||
v.Children[0].writeTo(buf, flags.set(prettyTop, false), indent, fmtstr)
|
||||
}
|
||||
case reflect.UnsafePointer:
|
||||
if len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "unsafe.Pointer(nil)")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "unsafe.Pointer(%#x)", v.Children[0].Addr)
|
||||
}
|
||||
case reflect.Chan:
|
||||
if flags.newlines() {
|
||||
v.writeStructTo(buf, flags, indent, fmtstr)
|
||||
} else {
|
||||
if len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%s %s/%s", v.typeStr(flags), v.Children[0].Value, v.Children[1].Value)
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
if v.Value != "" {
|
||||
fmt.Fprintf(buf, "%s(%s)", v.typeStr(flags), v.Value)
|
||||
flags = flags.set(prettyIncludeType, false)
|
||||
}
|
||||
v.writeStructTo(buf, flags, indent, fmtstr)
|
||||
case reflect.Interface:
|
||||
if v.Addr == 0 {
|
||||
// an escaped interface variable that points to nil, this shouldn't
|
||||
// happen in normal code but can happen if the variable is out of scope.
|
||||
fmt.Fprintf(buf, "nil")
|
||||
return
|
||||
}
|
||||
if flags.includeType() {
|
||||
if v.Children[0].Kind == reflect.Invalid {
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
if v.Children[0].Addr == 0 {
|
||||
fmt.Fprint(buf, "nil")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%s(%s) ", v.typeStr(flags), v.Children[0].Type)
|
||||
}
|
||||
}
|
||||
data := v.Children[0]
|
||||
if data.Kind == reflect.Ptr {
|
||||
if len(data.Children) == 0 {
|
||||
fmt.Fprint(buf, "...")
|
||||
} else if data.Children[0].Addr == 0 {
|
||||
fmt.Fprint(buf, "nil")
|
||||
} else if data.Children[0].OnlyAddr {
|
||||
fmt.Fprintf(buf, "0x%x", v.Children[0].Addr)
|
||||
} else {
|
||||
v.Children[0].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, !flags.includeType()), indent, fmtstr)
|
||||
}
|
||||
} else if data.OnlyAddr {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "*(*%q)(%#x)", v.typeStr(flags), v.Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "*(*%s)(%#x)", v.typeStr(flags), v.Addr)
|
||||
}
|
||||
} else {
|
||||
v.Children[0].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, !flags.includeType()), indent, fmtstr)
|
||||
}
|
||||
case reflect.Map:
|
||||
v.writeMapTo(buf, flags, indent, fmtstr)
|
||||
case reflect.Func:
|
||||
if v.Value == "" {
|
||||
fmt.Fprint(buf, "nil")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%s", v.Value)
|
||||
if flags.newlines() && len(v.Children) > 0 {
|
||||
fmt.Fprintf(buf, " {\n")
|
||||
for i := range v.Children {
|
||||
fmt.Fprintf(buf, "%s%s%s %s = ", indent, indentString, v.Children[i].Name, v.Children[i].typeStr(flags))
|
||||
v.Children[i].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, false), indent+indentString, fmtstr)
|
||||
fmt.Fprintf(buf, "\n")
|
||||
}
|
||||
fmt.Fprintf(buf, "%s}", indent)
|
||||
}
|
||||
}
|
||||
default:
|
||||
v.writeBasicType(buf, fmtstr)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) writePointerTo(buf io.Writer, flags prettyFlags) {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "(%q)(%#x)", v.typeStr(flags), v.Children[0].Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "(%s)(%#x)", v.typeStr(flags), v.Children[0].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) writeBasicType(buf io.Writer, fmtstr string) {
|
||||
if v.Value == "" && v.Kind != reflect.String {
|
||||
fmt.Fprintf(buf, "(unknown %s)", v.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
switch v.Kind {
|
||||
case reflect.Bool:
|
||||
if fmtstr == "" {
|
||||
buf.Write([]byte(v.Value))
|
||||
return
|
||||
}
|
||||
var b bool = v.Value == "true"
|
||||
fmt.Fprintf(buf, fmtstr, b)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if fmtstr == "" {
|
||||
buf.Write([]byte(v.Value))
|
||||
return
|
||||
}
|
||||
n, _ := strconv.ParseInt(ExtractIntValue(v.Value), 10, 64)
|
||||
fmt.Fprintf(buf, fmtstr, n)
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if fmtstr == "" {
|
||||
buf.Write([]byte(v.Value))
|
||||
return
|
||||
}
|
||||
n, _ := strconv.ParseUint(ExtractIntValue(v.Value), 10, 64)
|
||||
fmt.Fprintf(buf, fmtstr, n)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if fmtstr == "" {
|
||||
buf.Write([]byte(v.Value))
|
||||
return
|
||||
}
|
||||
x, _ := strconv.ParseFloat(v.Value, 64)
|
||||
fmt.Fprintf(buf, fmtstr, x)
|
||||
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
if fmtstr == "" {
|
||||
fmt.Fprintf(buf, "(%s + %si)", v.Children[0].Value, v.Children[1].Value)
|
||||
return
|
||||
}
|
||||
real, _ := strconv.ParseFloat(v.Children[0].Value, 64)
|
||||
imag, _ := strconv.ParseFloat(v.Children[1].Value, 64)
|
||||
var x complex128 = complex(real, imag)
|
||||
fmt.Fprintf(buf, fmtstr, x)
|
||||
|
||||
case reflect.String:
|
||||
if fmtstr == "" {
|
||||
s := v.Value
|
||||
if len(s) != int(v.Len) {
|
||||
s = fmt.Sprintf("%s...+%d more", s, int(v.Len)-len(s))
|
||||
}
|
||||
fmt.Fprintf(buf, "%q", s)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(buf, fmtstr, v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractIntValue(s string) string {
|
||||
if s == "" || s[len(s)-1] != ')' {
|
||||
return s
|
||||
}
|
||||
open := strings.LastIndex(s, "(")
|
||||
if open < 0 {
|
||||
return s
|
||||
}
|
||||
return s[open+1 : len(s)-1]
|
||||
}
|
||||
|
||||
func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s len: %d, cap: %d, ", v.typeStr(flags), v.Len, v.Cap)
|
||||
}
|
||||
if v.Base == 0 && len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "nil")
|
||||
return
|
||||
}
|
||||
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
|
||||
}
|
||||
|
||||
func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
|
||||
}
|
||||
|
||||
func (v *Variable) writeStructTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if int(v.Len) != len(v.Children) && len(v.Children) == 0 {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "(*%q)(%#x)", v.typeStr(flags), v.Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "(*%s)(%#x)", v.typeStr(flags), v.Addr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
|
||||
nl := v.shouldNewlineStruct(flags.newlines())
|
||||
|
||||
fmt.Fprint(buf, "{")
|
||||
|
||||
for i := range v.Children {
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
}
|
||||
fmt.Fprintf(buf, "%s: ", v.Children[i].Name)
|
||||
v.Children[i].writeTo(buf, prettyIncludeType.set(prettyNewlines, nl), indent+indentString, fmtstr)
|
||||
if i != len(v.Children)-1 || nl {
|
||||
fmt.Fprint(buf, ",")
|
||||
if !nl {
|
||||
fmt.Fprint(buf, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Children) != int(v.Len) {
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
} else {
|
||||
fmt.Fprint(buf, ",")
|
||||
}
|
||||
fmt.Fprintf(buf, "...+%d more", int(v.Len)-len(v.Children))
|
||||
}
|
||||
|
||||
fmt.Fprint(buf, "}")
|
||||
}
|
||||
|
||||
func (v *Variable) writeMapTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
if v.Base == 0 && len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "nil")
|
||||
return
|
||||
}
|
||||
|
||||
nl := flags.newlines() && (len(v.Children) > 0)
|
||||
|
||||
fmt.Fprint(buf, "[")
|
||||
|
||||
for i := 0; i < len(v.Children); i += 2 {
|
||||
key := &v.Children[i]
|
||||
value := &v.Children[i+1]
|
||||
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
}
|
||||
|
||||
key.writeTo(buf, 0, indent+indentString, fmtstr)
|
||||
fmt.Fprint(buf, ": ")
|
||||
value.writeTo(buf, prettyFlags(0).set(prettyNewlines, nl), indent+indentString, fmtstr)
|
||||
if i != len(v.Children)-1 || nl {
|
||||
fmt.Fprint(buf, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Children)/2 != int(v.Len) {
|
||||
if len(v.Children) != 0 {
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
} else {
|
||||
fmt.Fprint(buf, ",")
|
||||
}
|
||||
fmt.Fprintf(buf, "...+%d more", int(v.Len)-(len(v.Children)/2))
|
||||
} else {
|
||||
fmt.Fprint(buf, "...")
|
||||
}
|
||||
}
|
||||
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s", indent)
|
||||
}
|
||||
fmt.Fprint(buf, "]")
|
||||
}
|
||||
|
||||
func (v *Variable) shouldNewlineArray(newlines bool) bool {
|
||||
if !newlines || len(v.Children) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
kind, hasptr := (&v.Children[0]).recursiveKind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map, reflect.Interface:
|
||||
return true
|
||||
case reflect.String:
|
||||
if hasptr {
|
||||
return true
|
||||
}
|
||||
for i := range v.Children {
|
||||
if len(v.Children[i].Value) > maxShortStringLen {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) recursiveKind() (reflect.Kind, bool) {
|
||||
hasptr := false
|
||||
var kind reflect.Kind
|
||||
for {
|
||||
kind = v.Kind
|
||||
if kind == reflect.Ptr {
|
||||
hasptr = true
|
||||
if len(v.Children) == 0 {
|
||||
return kind, hasptr
|
||||
}
|
||||
v = &(v.Children[0])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return kind, hasptr
|
||||
}
|
||||
|
||||
func (v *Variable) shouldNewlineStruct(newlines bool) bool {
|
||||
if !newlines || len(v.Children) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range v.Children {
|
||||
kind, hasptr := (&v.Children[i]).recursiveKind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map, reflect.Interface:
|
||||
return true
|
||||
case reflect.String:
|
||||
if hasptr {
|
||||
return true
|
||||
}
|
||||
if len(v.Children[i].Value) > maxShortStringLen {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *Variable) writeSliceOrArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
nl := v.shouldNewlineArray(flags.newlines())
|
||||
fmt.Fprint(buf, "[")
|
||||
|
||||
for i := range v.Children {
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
}
|
||||
v.Children[i].writeTo(buf, prettyFlags(0).set(prettyNewlines, nl), indent+indentString, fmtstr)
|
||||
if i != len(v.Children)-1 || nl {
|
||||
fmt.Fprint(buf, ",")
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Children) != int(v.Len) {
|
||||
if len(v.Children) != 0 {
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
|
||||
} else {
|
||||
fmt.Fprint(buf, ",")
|
||||
}
|
||||
fmt.Fprintf(buf, "...+%d more", int(v.Len)-len(v.Children))
|
||||
} else {
|
||||
fmt.Fprint(buf, "...")
|
||||
}
|
||||
}
|
||||
|
||||
if nl {
|
||||
fmt.Fprintf(buf, "\n%s", indent)
|
||||
}
|
||||
|
||||
fmt.Fprint(buf, "]")
|
||||
}
|
||||
|
||||
// PrettyExamineMemory examine the memory and format data
|
||||
//
|
||||
// `format` specifies the data format (or data type), `size` specifies size of each data,
|
||||
// like 4byte integer, 1byte character, etc. `count` specifies the number of values.
|
||||
func PrettyExamineMemory(address uintptr, memArea []byte, isLittleEndian bool, format byte, size int) string {
|
||||
var (
|
||||
cols int
|
||||
colFormat string
|
||||
colBytes = size
|
||||
|
||||
addrLen int
|
||||
addrFmt string
|
||||
)
|
||||
|
||||
// Different versions of golang output differently about '#'.
|
||||
// See https://ci.appveyor.com/project/derekparker/delve-facy3/builds/30179356.
|
||||
switch format {
|
||||
case 'b':
|
||||
cols = 4 // Avoid emitting rows that are too long when using binary format
|
||||
colFormat = fmt.Sprintf("%%0%db", colBytes*8)
|
||||
case 'o':
|
||||
cols = 8
|
||||
colFormat = fmt.Sprintf("0%%0%do", colBytes*3) // Always keep one leading zero for octal.
|
||||
case 'd':
|
||||
cols = 8
|
||||
colFormat = fmt.Sprintf("%%0%dd", colBytes*3)
|
||||
case 'x':
|
||||
cols = 8
|
||||
colFormat = fmt.Sprintf("0x%%0%dx", colBytes*2) // Always keep one leading '0x' for hex.
|
||||
default:
|
||||
return fmt.Sprintf("not supported format %q\n", string(format))
|
||||
}
|
||||
colFormat += "\t"
|
||||
|
||||
l := len(memArea)
|
||||
rows := l / (cols * colBytes)
|
||||
if l%(cols*colBytes) != 0 {
|
||||
rows++
|
||||
}
|
||||
|
||||
// Avoid the lens of two adjacent address are different, so always use the last addr's len to format.
|
||||
if l != 0 {
|
||||
addrLen = len(fmt.Sprintf("%x", uint64(address)+uint64(l)))
|
||||
}
|
||||
addrFmt = "0x%0" + strconv.Itoa(addrLen) + "x:\t"
|
||||
|
||||
var b strings.Builder
|
||||
w := tabwriter.NewWriter(&b, 0, 0, 3, ' ', 0)
|
||||
|
||||
for i := 0; i < rows; i++ {
|
||||
fmt.Fprintf(w, addrFmt, address)
|
||||
|
||||
for j := 0; j < cols; j++ {
|
||||
offset := i*(cols*colBytes) + j*colBytes
|
||||
if offset+colBytes <= len(memArea) {
|
||||
n := byteArrayToUInt64(memArea[offset:offset+colBytes], isLittleEndian)
|
||||
fmt.Fprintf(w, colFormat, n)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, "")
|
||||
address += uintptr(cols * colBytes)
|
||||
}
|
||||
w.Flush()
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func byteArrayToUInt64(buf []byte, isLittleEndian bool) uint64 {
|
||||
var n uint64
|
||||
if isLittleEndian {
|
||||
for i := len(buf) - 1; i >= 0; i-- {
|
||||
n = n<<8 + uint64(buf[i])
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < len(buf); i++ {
|
||||
n = n<<8 + uint64(buf[i])
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
const stacktraceTruncatedMessage = "(truncated)"
|
||||
|
||||
func digits(n int) int {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
return int(math.Floor(math.Log10(float64(n)))) + 1
|
||||
}
|
||||
|
||||
type StackTraceColors struct {
|
||||
FunctionColor string
|
||||
BasenameColor string
|
||||
NormalColor string
|
||||
}
|
||||
|
||||
func PrintStack(formatPath func(string) string, out io.Writer, stack []Stackframe, ind string, offsets bool, stc StackTraceColors, include func(Stackframe) bool) {
|
||||
if len(stack) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
extranl := offsets
|
||||
for i := range stack {
|
||||
if extranl {
|
||||
break
|
||||
}
|
||||
extranl = extranl || (len(stack[i].Defers) > 0) || (len(stack[i].Arguments) > 0) || (len(stack[i].Locals) > 0)
|
||||
}
|
||||
|
||||
fileLine := func(file string, line int) string {
|
||||
file = formatPath(file)
|
||||
if stc.BasenameColor == "" {
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
slash := strings.LastIndex(file, "/")
|
||||
if slash < 0 {
|
||||
slash = 0
|
||||
} else {
|
||||
slash++
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s:%d%s", file[:slash], stc.BasenameColor, file[slash:], line, stc.NormalColor)
|
||||
}
|
||||
|
||||
d := digits(len(stack) - 1)
|
||||
fmtstr := "%s%" + strconv.Itoa(d) + "d 0x%016x in " + stc.FunctionColor + "%s" + stc.NormalColor + "\n"
|
||||
s := ind + strings.Repeat(" ", d+2+len(ind))
|
||||
|
||||
for i := range stack {
|
||||
if !include(stack[i]) {
|
||||
continue
|
||||
}
|
||||
if stack[i].Err != "" {
|
||||
fmt.Fprintf(out, "%serror: %s\n", s, stack[i].Err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(out, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
|
||||
fmt.Fprintf(out, "%sat %s\n", s, fileLine(stack[i].File, stack[i].Line))
|
||||
|
||||
if offsets {
|
||||
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.Fprintf(out, "%s(unreadable defer: %s)\n", deferHeader, d.Unreadable)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(out, "%s%#016x in %s%s%s\n", deferHeader, d.DeferredLoc.PC, stc.FunctionColor, d.DeferredLoc.Function.Name(), stc.NormalColor)
|
||||
fmt.Fprintf(out, "%sat %s:%d\n", s2, formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
|
||||
fmt.Fprintf(out, "%sdeferred by %s at %s\n", s2, d.DeferLoc.Function.Name(), fileLine(d.DeferLoc.File, d.DeferLoc.Line))
|
||||
}
|
||||
|
||||
for j := range stack[i].Arguments {
|
||||
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.Fprintf(out, "%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].SinglelineString())
|
||||
}
|
||||
|
||||
if extranl {
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
}
|
||||
|
||||
if len(stack) > 0 && !stack[len(stack)-1].Bottom {
|
||||
fmt.Fprintf(out, "%s"+stacktraceTruncatedMessage+"\n", ind)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrettyExamineMemory(t *testing.T) {
|
||||
// Test whether always use the last addr's len to format when the lens of two adjacent address are different
|
||||
addr := uintptr(0xffff)
|
||||
memArea := []byte("abcdefghijklmnopqrstuvwxyz")
|
||||
format := byte('o')
|
||||
|
||||
display := []string{
|
||||
"0x0ffff: 0141 0142 0143 0144 0145 0146 0147 0150 ",
|
||||
"0x10007: 0151 0152 0153 0154 0155 0156 0157 0160 ",
|
||||
"0x1000f: 0161 0162 0163 0164 0165 0166 0167 0170 ",
|
||||
"0x10017: 0171 0172"}
|
||||
res := strings.Split(strings.TrimSpace(PrettyExamineMemory(addr, memArea, true, format, 1)), "\n")
|
||||
|
||||
if len(display) != len(res) {
|
||||
t.Fatalf("wrong lines return, expected %d but got %d", len(display), len(res))
|
||||
}
|
||||
|
||||
for i := 0; i < len(display); i++ {
|
||||
if display[i] != res[i] {
|
||||
errInfo := fmt.Sprintf("wrong display return at line %d\n", i+1)
|
||||
errInfo += fmt.Sprintf("expected:\n %q\n", display[i])
|
||||
errInfo += fmt.Sprintf("but got:\n %q\n", res[i])
|
||||
t.Fatal(errInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_byteArrayToUInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []byte
|
||||
want uint64
|
||||
}{
|
||||
{"case-nil", nil, 0},
|
||||
{"case-empty", []byte{}, 0},
|
||||
{"case-1", []byte{0x1}, 1},
|
||||
{"case-2", []byte{0x12}, 18},
|
||||
{"case-3", []byte{0x1, 0x2}, 513},
|
||||
{"case-4", []byte{0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2}, 144397766876004609},
|
||||
{"case-5", []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, math.MaxUint64},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := byteArrayToUInt64(tt.args, true)
|
||||
if got != tt.want {
|
||||
t.Errorf("byteArrayToUInt64() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func ShortenType(typ string) string {
|
||||
out, ok := shortenTypeEx(typ)
|
||||
if !ok {
|
||||
return typ
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func shortenTypeEx(typ string) (string, bool) {
|
||||
switch {
|
||||
case strings.HasPrefix(typ, "["):
|
||||
for i := range typ {
|
||||
if typ[i] == ']' {
|
||||
sub, ok := shortenTypeEx(typ[i+1:])
|
||||
return typ[:i+1] + sub, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
case strings.HasPrefix(typ, "*"):
|
||||
sub, ok := shortenTypeEx(typ[1:])
|
||||
return "*" + sub, ok
|
||||
case strings.HasPrefix(typ, "map["):
|
||||
depth := 1
|
||||
for i := 4; i < len(typ); i++ {
|
||||
switch typ[i] {
|
||||
case '[':
|
||||
depth++
|
||||
case ']':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
key, keyok := shortenTypeEx(typ[4:i])
|
||||
val, valok := shortenTypeEx(typ[i+1:])
|
||||
return "map[" + key + "]" + val, keyok && valok
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
case typ == "interface {}" || typ == "interface{}":
|
||||
return typ, true
|
||||
case typ == "struct {}" || typ == "struct{}":
|
||||
return typ, true
|
||||
default:
|
||||
if containsAnonymousType(typ) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if lbrk := strings.Index(typ, "["); lbrk >= 0 {
|
||||
if typ[len(typ)-1] != ']' {
|
||||
return "", false
|
||||
}
|
||||
typ0, ok := shortenTypeEx(typ[:lbrk])
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
args := strings.Split(typ[lbrk+1:len(typ)-1], ",")
|
||||
for i := range args {
|
||||
var ok bool
|
||||
args[i], ok = shortenTypeEx(strings.TrimSpace(args[i]))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return typ0 + "[" + strings.Join(args, ", ") + "]", true
|
||||
}
|
||||
|
||||
slashnum := 0
|
||||
slash := -1
|
||||
for i, ch := range typ {
|
||||
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '.' && ch != '/' && ch != '@' && ch != '%' && ch != '-' {
|
||||
return "", false
|
||||
}
|
||||
if ch == '/' {
|
||||
slash = i
|
||||
slashnum++
|
||||
}
|
||||
}
|
||||
if slashnum <= 1 || slash < 0 {
|
||||
return typ, true
|
||||
}
|
||||
return typ[slash+1:], true
|
||||
}
|
||||
}
|
||||
|
||||
func containsAnonymousType(typ string) bool {
|
||||
for _, thing := range []string{"interface {", "interface{", "struct {", "struct{", "func (", "func("} {
|
||||
idx := strings.Index(typ, thing)
|
||||
if idx >= 0 && idx+len(thing) < len(typ) {
|
||||
ch := typ[idx+len(thing)]
|
||||
if ch != '}' && ch != ')' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShortenType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"long/package/path/pkg.A", "pkg.A"},
|
||||
{"[]long/package/path/pkg.A", "[]pkg.A"},
|
||||
{"map[long/package/path/pkg.A]long/package/path/pkg.B", "map[pkg.A]pkg.B"},
|
||||
{"map[long/package/path/pkg.A]interface {}", "map[pkg.A]interface {}"},
|
||||
{"map[long/package/path/pkg.A]interface{}", "map[pkg.A]interface{}"},
|
||||
{"map[long/package/path/pkg.A]struct {}", "map[pkg.A]struct {}"},
|
||||
{"map[long/package/path/pkg.A]struct{}", "map[pkg.A]struct{}"},
|
||||
{"map[long/package/path/pkg.A]map[long/package/path/pkg.B]long/package/path/pkg.C", "map[pkg.A]map[pkg.B]pkg.C"},
|
||||
{"map[long/package/path/pkg.A][]long/package/path/pkg.B", "map[pkg.A][]pkg.B"},
|
||||
{"map[uint64]*github.com/aarzilli/dwarf5/dwarf.typeUnit", "map[uint64]*dwarf.typeUnit"},
|
||||
{"uint8", "uint8"},
|
||||
{"encoding/binary", "encoding/binary"},
|
||||
{"*github.com/go-delve/delve/pkg/proc.Target", "*proc.Target"},
|
||||
{"long/package/path/pkg.Parametric[long/package/path/pkg.A, map[long/package/path/pkg.B]long/package/path/pkg.A]", "pkg.Parametric[pkg.A, map[pkg.B]pkg.A]"},
|
||||
{"[]long/package/path/pkg.Parametric[long/package/path/pkg.A]", "[]pkg.Parametric[pkg.A]"},
|
||||
{"[24]long/package/path/pkg.A", "[24]pkg.A"},
|
||||
{"chan func()", "chan func()"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := ShortenType(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("shortenTypeEx() got = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
"github.com/go-delve/delve/pkg/proc"
|
||||
)
|
||||
|
||||
// ErrNotExecutable is an error returned when trying
|
||||
// to debug a non-executable file.
|
||||
var ErrNotExecutable = errors.New("not an executable file")
|
||||
|
||||
// DebuggerState represents the current context of the debugger.
|
||||
type DebuggerState struct {
|
||||
// PID of the process we are debugging.
|
||||
Pid int
|
||||
// Command line of the process we are debugging.
|
||||
TargetCommandLine string
|
||||
// Running is true if the process is running and no other information can be collected.
|
||||
Running bool
|
||||
// Recording is true if the process is currently being recorded and no other
|
||||
// information can be collected. While the debugger is in this state
|
||||
// sending a StopRecording request will halt the recording, every other
|
||||
// request will block until the process has been recorded.
|
||||
Recording bool
|
||||
// Core dumping currently in progress.
|
||||
CoreDumping bool
|
||||
// CurrentThread is the currently selected debugger thread.
|
||||
CurrentThread *Thread `json:"currentThread,omitempty"`
|
||||
// SelectedGoroutine is the currently selected goroutine
|
||||
SelectedGoroutine *Goroutine `json:"currentGoroutine,omitempty"`
|
||||
// List of all the process threads
|
||||
Threads []*Thread
|
||||
// NextInProgress indicates that a next or step operation was interrupted by another breakpoint
|
||||
// or a manual stop and is waiting to complete.
|
||||
// While NextInProgress is set further requests for next or step may be rejected.
|
||||
// Either execute continue until NextInProgress is false or call CancelNext
|
||||
NextInProgress bool
|
||||
// WatchOutOfScope contains the list of watchpoints that went out of scope
|
||||
// during the last continue.
|
||||
WatchOutOfScope []*Breakpoint
|
||||
// Exited indicates whether the debugged process has exited.
|
||||
Exited bool `json:"exited"`
|
||||
ExitStatus int `json:"exitStatus"`
|
||||
// When contains a description of the current position in a recording
|
||||
When string
|
||||
// Filled by RPCClient.Continue, indicates an error
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
type TracepointResult struct {
|
||||
// Addr is the address of this tracepoint.
|
||||
Addr uint64 `json:"addr"`
|
||||
// File is the source file for the breakpoint.
|
||||
File string `json:"file"`
|
||||
// Line is a line in File for the breakpoint.
|
||||
Line int `json:"line"`
|
||||
IsRet bool `json:"is_ret"`
|
||||
// FunctionName is the name of the function at the current breakpoint, and
|
||||
// may not always be available.
|
||||
FunctionName string `json:"functionName,omitempty"`
|
||||
|
||||
GoroutineID int `json:"goroutineID"`
|
||||
|
||||
InputParams []Variable `json:"inputParams,omitempty"`
|
||||
ReturnParams []Variable `json:"returnParams,omitempty"`
|
||||
}
|
||||
|
||||
// Breakpoint addresses a set of locations at which process execution may be
|
||||
// suspended.
|
||||
type Breakpoint struct {
|
||||
// ID is a unique identifier for the breakpoint.
|
||||
ID int `json:"id"`
|
||||
// User defined name of the breakpoint.
|
||||
Name string `json:"name"`
|
||||
// Addr is deprecated, use Addrs.
|
||||
Addr uint64 `json:"addr"`
|
||||
// Addrs is the list of addresses for this breakpoint.
|
||||
Addrs []uint64 `json:"addrs"`
|
||||
// AddrPid[i] is the PID associated with by Addrs[i], when debugging a
|
||||
// single target process this is optional, otherwise it is mandatory.
|
||||
AddrPid []int `json:"addrpid"`
|
||||
// File is the source file for the breakpoint.
|
||||
File string `json:"file"`
|
||||
// Line is a line in File for the breakpoint.
|
||||
Line int `json:"line"`
|
||||
// FunctionName is the name of the function at the current breakpoint, and
|
||||
// may not always be available.
|
||||
FunctionName string `json:"functionName,omitempty"`
|
||||
// ExprString is the string that will be used to set a suspended breakpoint.
|
||||
ExprString string
|
||||
|
||||
// Breakpoint condition
|
||||
Cond string
|
||||
// Breakpoint hit count condition.
|
||||
// Supported hit count conditions are "NUMBER" and "OP NUMBER".
|
||||
HitCond string
|
||||
// HitCondPerG use per goroutine hitcount as HitCond operand, instead of total hitcount
|
||||
HitCondPerG bool
|
||||
|
||||
// Tracepoint flag, signifying this is a tracepoint.
|
||||
Tracepoint bool `json:"continue"`
|
||||
// TraceReturn flag signifying this is a breakpoint set at a return
|
||||
// statement in a traced function.
|
||||
TraceReturn bool `json:"traceReturn"`
|
||||
// retrieve goroutine information
|
||||
Goroutine bool `json:"goroutine"`
|
||||
// number of stack frames to retrieve
|
||||
Stacktrace int `json:"stacktrace"`
|
||||
// expressions to evaluate
|
||||
Variables []string `json:"variables,omitempty"`
|
||||
// LoadArgs requests loading function arguments when the breakpoint is hit
|
||||
LoadArgs *LoadConfig
|
||||
// LoadLocals requests loading function locals when the breakpoint is hit
|
||||
LoadLocals *LoadConfig
|
||||
|
||||
// WatchExpr is the expression used to create this watchpoint
|
||||
WatchExpr string
|
||||
WatchType WatchType
|
||||
|
||||
VerboseDescr []string `json:"VerboseDescr,omitempty"`
|
||||
|
||||
// number of times a breakpoint has been reached in a certain goroutine
|
||||
HitCount map[string]uint64 `json:"hitCount"`
|
||||
// number of times a breakpoint has been reached
|
||||
TotalHitCount uint64 `json:"totalHitCount"`
|
||||
// Disabled flag, signifying the state of the breakpoint
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
UserData interface{} `json:"-"`
|
||||
|
||||
// RootFuncName is the Root function from where tracing needs to be done
|
||||
RootFuncName string
|
||||
// TraceFollowCalls indicates the Depth of tracing
|
||||
TraceFollowCalls int
|
||||
}
|
||||
|
||||
// ValidBreakpointName returns an error if
|
||||
// the name to be chosen for a breakpoint is invalid.
|
||||
// The name can not be just a number, and must contain a series
|
||||
// of letters or numbers.
|
||||
func ValidBreakpointName(name string) error {
|
||||
if _, err := strconv.Atoi(name); err == nil {
|
||||
return errors.New("breakpoint name can not be a number")
|
||||
}
|
||||
|
||||
for _, ch := range name {
|
||||
if !(unicode.IsLetter(ch) || unicode.IsDigit(ch)) {
|
||||
return fmt.Errorf("invalid character in breakpoint name '%c'", ch)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WatchType is the watchpoint type
|
||||
type WatchType uint8
|
||||
|
||||
const (
|
||||
WatchRead WatchType = 1 << iota
|
||||
WatchWrite
|
||||
)
|
||||
|
||||
// Thread is a thread within the debugged process.
|
||||
type Thread struct {
|
||||
// ID is a unique identifier for the thread.
|
||||
ID int `json:"id"`
|
||||
// PC is the current program counter for the thread.
|
||||
PC uint64 `json:"pc"`
|
||||
// File is the file for the program counter.
|
||||
File string `json:"file"`
|
||||
// Line is the line number for the program counter.
|
||||
Line int `json:"line"`
|
||||
// Function is function information at the program counter. May be nil.
|
||||
Function *Function `json:"function,omitempty"`
|
||||
|
||||
// ID of the goroutine running on this thread
|
||||
GoroutineID int64 `json:"goroutineID"`
|
||||
|
||||
// Breakpoint this thread is stopped at
|
||||
Breakpoint *Breakpoint `json:"breakPoint,omitempty"`
|
||||
// Information requested by the current breakpoint
|
||||
BreakpointInfo *BreakpointInfo `json:"breakPointInfo,omitempty"`
|
||||
|
||||
// ReturnValues contains the return values of the function we just stepped out of
|
||||
ReturnValues []Variable
|
||||
// CallReturn is true if ReturnValues are the return values of an injected call.
|
||||
CallReturn bool
|
||||
}
|
||||
|
||||
// Location holds program location information.
|
||||
// In most cases a Location object will represent a physical location, with
|
||||
// a single PC address held in the PC field.
|
||||
// FindLocations however returns logical locations that can either have
|
||||
// multiple PC addresses each (due to inlining) or no PC address at all.
|
||||
type Location struct {
|
||||
PC uint64 `json:"pc"`
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
Function *Function `json:"function,omitempty"`
|
||||
PCs []uint64 `json:"pcs,omitempty"`
|
||||
PCPids []int `json:"pcpids,omitempty"`
|
||||
}
|
||||
|
||||
// Stackframe describes one frame in a stack trace.
|
||||
type Stackframe struct {
|
||||
Location
|
||||
Locals []Variable
|
||||
Arguments []Variable
|
||||
|
||||
FrameOffset int64
|
||||
FramePointerOffset int64
|
||||
|
||||
Defers []Defer
|
||||
|
||||
Bottom bool `json:"Bottom,omitempty"` // Bottom is true if this is the bottom frame of the stack
|
||||
|
||||
Err string
|
||||
}
|
||||
|
||||
// Defer describes a deferred function.
|
||||
type Defer struct {
|
||||
DeferredLoc Location // deferred function
|
||||
DeferLoc Location // location of the defer statement
|
||||
SP uint64 // value of SP when the function was deferred
|
||||
Unreadable string
|
||||
}
|
||||
|
||||
// Var will return the variable described by 'name' within
|
||||
// this stack frame.
|
||||
func (frame *Stackframe) Var(name string) *Variable {
|
||||
for i := range frame.Locals {
|
||||
if frame.Locals[i].Name == name {
|
||||
return &frame.Locals[i]
|
||||
}
|
||||
}
|
||||
for i := range frame.Arguments {
|
||||
if frame.Arguments[i].Name == name {
|
||||
return &frame.Arguments[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Function represents thread-scoped function information.
|
||||
type Function struct {
|
||||
// Name is the function name.
|
||||
Name_ string `json:"name"`
|
||||
Value uint64 `json:"value"`
|
||||
Type byte `json:"type"`
|
||||
GoType uint64 `json:"goType"`
|
||||
// Optimized is true if the function was optimized
|
||||
Optimized bool `json:"optimized"`
|
||||
}
|
||||
|
||||
// Name will return the function name.
|
||||
func (fn *Function) Name() string {
|
||||
if fn == nil {
|
||||
return "???"
|
||||
}
|
||||
return fn.Name_
|
||||
}
|
||||
|
||||
// VariableFlags is the type of the Flags field of Variable.
|
||||
type VariableFlags uint16
|
||||
|
||||
const (
|
||||
// VariableEscaped is set for local variables that escaped to the heap
|
||||
//
|
||||
// The compiler performs escape analysis on local variables, the variables
|
||||
// that may outlive the stack frame are allocated on the heap instead and
|
||||
// only the address is recorded on the stack. These variables will be
|
||||
// marked with this flag.
|
||||
VariableEscaped = 1 << iota
|
||||
|
||||
// VariableShadowed is set for local variables that are shadowed by a
|
||||
// variable with the same name in another scope
|
||||
VariableShadowed
|
||||
|
||||
// VariableConstant means this variable is a constant value
|
||||
VariableConstant
|
||||
|
||||
// VariableArgument means this variable is a function argument
|
||||
VariableArgument
|
||||
|
||||
// VariableReturnArgument means this variable is a function return value
|
||||
VariableReturnArgument
|
||||
|
||||
// VariableFakeAddress means the address of this variable is either fake
|
||||
// (i.e. the variable is partially or completely stored in a CPU register
|
||||
// and doesn't have a real address) or possibly no longer available (because
|
||||
// the variable is the return value of a function call and allocated on a
|
||||
// frame that no longer exists)
|
||||
VariableFakeAddress
|
||||
|
||||
// VariableCPtr means the variable is a C pointer
|
||||
VariableCPtr
|
||||
|
||||
// VariableCPURegister means this variable is a CPU register.
|
||||
VariableCPURegister
|
||||
)
|
||||
|
||||
// Variable describes a variable.
|
||||
type Variable struct {
|
||||
// Name of the variable or struct member
|
||||
Name string `json:"name"`
|
||||
// Address of the variable or struct member
|
||||
Addr uint64 `json:"addr"`
|
||||
// Only the address field is filled (result of evaluating expressions like &<expr>)
|
||||
OnlyAddr bool `json:"onlyAddr"`
|
||||
// Go type of the variable
|
||||
Type string `json:"type"`
|
||||
// Type of the variable after resolving any typedefs
|
||||
RealType string `json:"realType"`
|
||||
|
||||
Flags VariableFlags `json:"flags"`
|
||||
|
||||
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
|
||||
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 captured variables for functions
|
||||
Len int64 `json:"len"`
|
||||
// Cap value for slices
|
||||
Cap int64 `json:"cap"`
|
||||
|
||||
// Array and slice elements, member fields of structs, key/value pairs of maps, value of complex numbers, captured variables of functions.
|
||||
// The Name field in this slice will always be the empty string except for structs (when it will be the field name) and for complex numbers (when it will be "real" and "imaginary")
|
||||
// For maps each map entry will have to items in this slice, even numbered items will represent map keys and odd numbered items will represent their values
|
||||
// This field's length is capped at proc.maxArrayValues for slices and arrays and 2*proc.maxArrayValues for maps, in the circumstances where the cap takes effect len(Children) != Len
|
||||
// The other length cap applied to this field is related to maximum recursion depth, when the maximum recursion depth is reached this field is left empty, contrary to the previous one this cap also applies to structs (otherwise structs will always have all their member fields returned)
|
||||
Children []Variable `json:"children"`
|
||||
|
||||
// Base address of arrays, Base address of the backing array for slices (0 for nil slices)
|
||||
// Base address of the backing byte array for strings
|
||||
// address of the struct backing chan and map variables
|
||||
// address of the function entry point for function variables (0 for nil function pointers)
|
||||
Base uint64 `json:"base"`
|
||||
|
||||
// Unreadable addresses will have this field set
|
||||
Unreadable string `json:"unreadable"`
|
||||
|
||||
// LocationExpr describes the location expression of this variable's address
|
||||
LocationExpr string
|
||||
// DeclLine is the line number of this variable's declaration
|
||||
DeclLine int64
|
||||
}
|
||||
|
||||
// LoadConfig describes how to load values from target's memory
|
||||
type LoadConfig struct {
|
||||
// FollowPointers requests pointers to be automatically dereferenced.
|
||||
FollowPointers bool
|
||||
// MaxVariableRecurse is how far to recurse when evaluating nested types.
|
||||
MaxVariableRecurse int
|
||||
// MaxStringLen is the maximum number of bytes read from a string
|
||||
MaxStringLen int
|
||||
// MaxArrayValues is the maximum number of elements read from an array, a slice or a map.
|
||||
MaxArrayValues int
|
||||
// MaxStructFields is the maximum number of fields read from a struct, -1 will read all fields.
|
||||
MaxStructFields int
|
||||
}
|
||||
|
||||
// Goroutine represents the information relevant to Delve from the runtime's
|
||||
// internal G structure.
|
||||
type Goroutine struct {
|
||||
// ID is a unique identifier for the goroutine.
|
||||
ID int64 `json:"id"`
|
||||
// Current location of the goroutine
|
||||
CurrentLoc Location `json:"currentLoc"`
|
||||
// Current location of the goroutine, excluding calls inside runtime
|
||||
UserCurrentLoc Location `json:"userCurrentLoc"`
|
||||
// Location of the go instruction that started this goroutine
|
||||
GoStatementLoc Location `json:"goStatementLoc"`
|
||||
// Location of the starting function
|
||||
StartLoc Location `json:"startLoc"`
|
||||
// ID of the associated thread for running goroutines
|
||||
ThreadID int `json:"threadID"`
|
||||
Status uint64 `json:"status"`
|
||||
WaitSince int64 `json:"waitSince"`
|
||||
WaitReason int64 `json:"waitReason"`
|
||||
Unreadable string `json:"unreadable"`
|
||||
// Goroutine's pprof labels
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
GoroutineWaiting = proc.Gwaiting
|
||||
GoroutineSyscall = proc.Gsyscall
|
||||
)
|
||||
|
||||
// DebuggerCommand is a command which changes the debugger's execution state.
|
||||
type DebuggerCommand struct {
|
||||
// Name is the command to run.
|
||||
Name string `json:"name"`
|
||||
// ThreadID is used to specify which thread to use with the SwitchThread
|
||||
// command.
|
||||
ThreadID int `json:"threadID,omitempty"`
|
||||
// GoroutineID is used to specify which thread to use with the SwitchGoroutine
|
||||
// and Call commands.
|
||||
GoroutineID int64 `json:"goroutineID,omitempty"`
|
||||
// When ReturnInfoLoadConfig is not nil it will be used to load the value
|
||||
// of any return variables.
|
||||
ReturnInfoLoadConfig *LoadConfig
|
||||
// Expr is the expression argument for a Call command
|
||||
Expr string `json:"expr,omitempty"`
|
||||
|
||||
// UnsafeCall disables parameter escape checking for function calls.
|
||||
// Go objects can be allocated on the stack or on the heap. Heap objects
|
||||
// can be used by any goroutine; stack objects can only be used by the
|
||||
// goroutine that owns the stack they are allocated on and can not survive
|
||||
// the stack frame of allocation.
|
||||
// The Go compiler will use escape analysis to determine whether to
|
||||
// allocate an object on the stack or the heap.
|
||||
// When injecting a function call Delve will check that no address of a
|
||||
// stack allocated object is passed to the called function: this ensures
|
||||
// the rules for stack objects will not be violated.
|
||||
// If you are absolutely sure that the function you are calling will not
|
||||
// violate the rules about stack objects you can disable this safety check
|
||||
// by setting UnsafeCall to true.
|
||||
UnsafeCall bool `json:"unsafeCall,omitempty"`
|
||||
}
|
||||
|
||||
// BreakpointInfo contains information about the current breakpoint
|
||||
type BreakpointInfo struct {
|
||||
Stacktrace []Stackframe `json:"stacktrace,omitempty"`
|
||||
Goroutine *Goroutine `json:"goroutine,omitempty"`
|
||||
Variables []Variable `json:"variables,omitempty"`
|
||||
Arguments []Variable `json:"arguments,omitempty"`
|
||||
Locals []Variable `json:"locals,omitempty"`
|
||||
}
|
||||
|
||||
// EvalScope is the scope a command should
|
||||
// be evaluated in. Describes the goroutine and frame number.
|
||||
type EvalScope struct {
|
||||
GoroutineID int64
|
||||
Frame int
|
||||
DeferredCall int // when DeferredCall is n > 0 this eval scope is relative to the n-th deferred call in the current frame
|
||||
}
|
||||
|
||||
const (
|
||||
// Continue resumes process execution.
|
||||
Continue = "continue"
|
||||
// Rewind resumes process execution backwards (target must be a recording).
|
||||
Rewind = "rewind"
|
||||
// DirectionCongruentContinue resumes process execution, if a reverse next, step or stepout operation is in progress it will resume execution backward.
|
||||
DirectionCongruentContinue = "directionCongruentContinue"
|
||||
// Step continues to next source line, entering function calls.
|
||||
Step = "step"
|
||||
// ReverseStep continues backward to the previous line of source code, entering function calls.
|
||||
ReverseStep = "reverseStep"
|
||||
// StepOut continues to the return address of the current function
|
||||
StepOut = "stepOut"
|
||||
// ReverseStepOut continues backward to the caller of the current function.
|
||||
ReverseStepOut = "reverseStepOut"
|
||||
// StepInstruction continues for exactly 1 cpu instruction.
|
||||
StepInstruction = "stepInstruction"
|
||||
// NextInstruction continues for 1 cpu instruction, skipping over CALL instructions.
|
||||
NextInstruction = "nextInstruction"
|
||||
// ReverseStepInstruction reverses execution for exactly 1 cpu instruction.
|
||||
ReverseStepInstruction = "reverseStepInstruction"
|
||||
// ReverseNextInstruction reverses execution for 1 cpu instruction, skipping over CALL instructions.
|
||||
ReverseNextInstruction = "reverseNextInstruction"
|
||||
// Next continues to the next source line, not entering function calls.
|
||||
Next = "next"
|
||||
// ReverseNext continues backward to the previous line of source code, not entering function calls.
|
||||
ReverseNext = "reverseNext"
|
||||
// SwitchThread switches the debugger's current thread context.
|
||||
SwitchThread = "switchThread"
|
||||
// SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine
|
||||
SwitchGoroutine = "switchGoroutine"
|
||||
// Halt suspends the process.
|
||||
// The effect of Halt while the target process is stopped, or in the
|
||||
// process of stopping, is operating system and timing dependent. It will
|
||||
// either have no effect or cause the following resume to stop immediately.
|
||||
Halt = "halt"
|
||||
// Call resumes process execution injecting a function call.
|
||||
Call = "call"
|
||||
)
|
||||
|
||||
// AssemblyFlavour describes the output
|
||||
// of disassembled code.
|
||||
type AssemblyFlavour int
|
||||
|
||||
const (
|
||||
// GNUFlavour will disassemble using GNU assembly syntax.
|
||||
GNUFlavour = AssemblyFlavour(proc.GNUFlavour)
|
||||
// IntelFlavour will disassemble using Intel assembly syntax.
|
||||
IntelFlavour = AssemblyFlavour(proc.IntelFlavour)
|
||||
// GoFlavour will disassemble using Go assembly syntax.
|
||||
GoFlavour = AssemblyFlavour(proc.GoFlavour)
|
||||
)
|
||||
|
||||
// AsmInstruction represents one assembly instruction at some address
|
||||
type AsmInstruction struct {
|
||||
// Loc is the location of this instruction
|
||||
Loc Location
|
||||
// Destination of CALL instructions
|
||||
DestLoc *Location
|
||||
// Text is the formatted representation of the instruction
|
||||
Text string
|
||||
// Bytes is the instruction as read from memory
|
||||
Bytes []byte
|
||||
// If Breakpoint is true a breakpoint is set at this instruction
|
||||
Breakpoint bool
|
||||
// In AtPC is true this is the instruction the current thread is stopped at
|
||||
AtPC bool
|
||||
}
|
||||
|
||||
// AsmInstructions is a slice of single instructions.
|
||||
type AsmInstructions []AsmInstruction
|
||||
|
||||
// GetVersionIn is the argument for GetVersion.
|
||||
type GetVersionIn struct {
|
||||
}
|
||||
|
||||
// GetVersionOut is the result of GetVersion.
|
||||
type GetVersionOut struct {
|
||||
DelveVersion string
|
||||
APIVersion int
|
||||
Backend string // backend currently in use
|
||||
TargetGoVersion string
|
||||
|
||||
MinSupportedVersionOfGo string
|
||||
MaxSupportedVersionOfGo string
|
||||
}
|
||||
|
||||
// SetAPIVersionIn is the input for SetAPIVersion.
|
||||
type SetAPIVersionIn struct {
|
||||
APIVersion int
|
||||
}
|
||||
|
||||
// SetAPIVersionOut is the output for SetAPIVersion.
|
||||
type SetAPIVersionOut struct {
|
||||
}
|
||||
|
||||
// Register holds information on a CPU register.
|
||||
type Register struct {
|
||||
Name string
|
||||
Value string
|
||||
DwarfNumber int
|
||||
}
|
||||
|
||||
// Registers is a list of CPU registers.
|
||||
type Registers []Register
|
||||
|
||||
func (regs Registers) String() string {
|
||||
maxlen := 0
|
||||
for _, reg := range regs {
|
||||
if n := len(reg.Name); n > maxlen {
|
||||
maxlen = n
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for _, reg := range regs {
|
||||
fmt.Fprintf(&buf, "%*s = %s\n", maxlen, reg.Name, reg.Value)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// DiscardedBreakpoint is a breakpoint that is not
|
||||
// reinstated during a restart.
|
||||
type DiscardedBreakpoint struct {
|
||||
Breakpoint *Breakpoint
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Checkpoint is a point in the program that
|
||||
// can be returned to in certain execution modes.
|
||||
type Checkpoint struct {
|
||||
ID int
|
||||
When string
|
||||
Where string
|
||||
}
|
||||
|
||||
// Image represents a loaded shared object (go plugin or shared library)
|
||||
type Image struct {
|
||||
Path string
|
||||
Address uint64
|
||||
LoadError string
|
||||
}
|
||||
|
||||
// Ancestor represents a goroutine ancestor
|
||||
type Ancestor struct {
|
||||
ID int64
|
||||
Stack []Stackframe
|
||||
|
||||
Unreadable string
|
||||
}
|
||||
|
||||
// StacktraceOptions is the type of the Opts field of StacktraceIn that
|
||||
// configures the stacktrace.
|
||||
// Tracks proc.StacktraceOptions
|
||||
type StacktraceOptions uint16
|
||||
|
||||
const (
|
||||
// StacktraceReadDefers requests a stacktrace decorated with deferred calls
|
||||
// for each frame.
|
||||
StacktraceReadDefers StacktraceOptions = 1 << iota
|
||||
|
||||
// StacktraceSimple requests a stacktrace where no stack switches will be
|
||||
// attempted.
|
||||
StacktraceSimple
|
||||
|
||||
// StacktraceG requests a stacktrace starting with the register
|
||||
// values saved in the runtime.g structure.
|
||||
StacktraceG
|
||||
)
|
||||
|
||||
// PackageBuildInfo maps an import path to a directory path.
|
||||
type PackageBuildInfo struct {
|
||||
ImportPath string
|
||||
DirectoryPath string
|
||||
Files []string
|
||||
}
|
||||
|
||||
// DumpState describes the state of a core dump in progress
|
||||
type DumpState struct {
|
||||
Dumping bool
|
||||
AllDone bool
|
||||
|
||||
ThreadsDone, ThreadsTotal int
|
||||
MemDone, MemTotal uint64
|
||||
|
||||
Err string
|
||||
}
|
||||
|
||||
// ListGoroutinesFilter describes a filtering condition for the
|
||||
// ListGoroutines API call.
|
||||
type ListGoroutinesFilter struct {
|
||||
Kind GoroutineField
|
||||
Negated bool
|
||||
Arg string
|
||||
}
|
||||
|
||||
// GoroutineField allows referring to a field of a goroutine object.
|
||||
type GoroutineField uint8
|
||||
|
||||
const (
|
||||
GoroutineFieldNone GoroutineField = iota
|
||||
GoroutineCurrentLoc // the goroutine's CurrentLoc
|
||||
GoroutineUserLoc // the goroutine's UserLoc
|
||||
GoroutineGoLoc // the goroutine's GoStatementLoc
|
||||
GoroutineStartLoc // the goroutine's StartLoc
|
||||
GoroutineLabel // the goroutine's label
|
||||
GoroutineRunning // the goroutine is running
|
||||
GoroutineUser // the goroutine is a user goroutine
|
||||
GoroutineWaitingOnChannel // the goroutine is waiting on the channel specified by the argument
|
||||
)
|
||||
|
||||
// GoroutineGroup represents a group of goroutines in the return value of
|
||||
// the ListGoroutines API call.
|
||||
type GoroutineGroup struct {
|
||||
Name string // name of this group
|
||||
Offset int // start offset in the list of goroutines of this group
|
||||
Count int // number of goroutines that belong to this group in the list of goroutines
|
||||
Total int // total number of goroutines that belong to this group
|
||||
}
|
||||
|
||||
type GoroutineGroupingOptions struct {
|
||||
GroupBy GoroutineField
|
||||
GroupByKey string
|
||||
MaxGroupMembers int
|
||||
MaxGroups int
|
||||
}
|
||||
|
||||
// Target represents a debugging target.
|
||||
type Target struct {
|
||||
Pid int
|
||||
CmdLine string
|
||||
CurrentThread *Thread
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-delve/delve/service/api"
|
||||
)
|
||||
|
||||
// Client represents a debugger service client. All client methods are
|
||||
// synchronous.
|
||||
type Client interface {
|
||||
// ProcessPid returns the pid of the process we are debugging.
|
||||
ProcessPid() int
|
||||
|
||||
// BuildID returns the BuildID of the process' executable we are debugging.
|
||||
BuildID() string
|
||||
|
||||
// LastModified returns the time that the process' executable was modified.
|
||||
LastModified() time.Time
|
||||
|
||||
// Detach detaches the debugger, optionally killing the process.
|
||||
Detach(killProcess bool) error
|
||||
|
||||
// Restart restarts program. Set true if you want to rebuild the process we are debugging.
|
||||
Restart(rebuild bool) ([]api.DiscardedBreakpoint, error)
|
||||
// RestartFrom restarts program from the specified position.
|
||||
RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error)
|
||||
|
||||
// GetState returns the current debugger state.
|
||||
GetState() (*api.DebuggerState, error)
|
||||
// GetStateNonBlocking returns the current debugger state, returning immediately if the target is already running.
|
||||
GetStateNonBlocking() (*api.DebuggerState, error)
|
||||
|
||||
// Continue resumes process execution.
|
||||
Continue() <-chan *api.DebuggerState
|
||||
// Rewind resumes process execution backwards.
|
||||
Rewind() <-chan *api.DebuggerState
|
||||
// DirectionCongruentContinue resumes process execution, if a reverse next, step or stepout operation is in progress it will resume execution backward.
|
||||
DirectionCongruentContinue() <-chan *api.DebuggerState
|
||||
// Next continues to the next source line, not entering function calls.
|
||||
Next() (*api.DebuggerState, error)
|
||||
// ReverseNext continues backward to the previous line of source code, not entering function calls.
|
||||
ReverseNext() (*api.DebuggerState, error)
|
||||
// Step continues to the next source line, entering function calls.
|
||||
Step() (*api.DebuggerState, error)
|
||||
// ReverseStep continues backward to the previous line of source code, entering function calls.
|
||||
ReverseStep() (*api.DebuggerState, error)
|
||||
// StepOut continues to the return address of the current function.
|
||||
StepOut() (*api.DebuggerState, error)
|
||||
// ReverseStepOut continues backward to the caller of the current function.
|
||||
ReverseStepOut() (*api.DebuggerState, error)
|
||||
// Call resumes process execution while making a function call.
|
||||
Call(goroutineID int64, expr string, unsafe bool) (*api.DebuggerState, error)
|
||||
|
||||
// StepInstruction will step a single cpu instruction.
|
||||
StepInstruction(skipCalls bool) (*api.DebuggerState, error)
|
||||
// ReverseStepInstruction will reverse step a single cpu instruction.
|
||||
ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, error)
|
||||
// SwitchThread switches the current thread context.
|
||||
SwitchThread(threadID int) (*api.DebuggerState, error)
|
||||
// SwitchGoroutine switches the current goroutine (and the current thread as well)
|
||||
SwitchGoroutine(goroutineID int64) (*api.DebuggerState, error)
|
||||
// Halt suspends the process.
|
||||
Halt() (*api.DebuggerState, error)
|
||||
|
||||
// GetBreakpoint gets a breakpoint by ID.
|
||||
GetBreakpoint(id int) (*api.Breakpoint, error)
|
||||
// GetBreakpointByName gets a breakpoint by name.
|
||||
GetBreakpointByName(name string) (*api.Breakpoint, error)
|
||||
// CreateBreakpoint creates a new breakpoint.
|
||||
CreateBreakpoint(*api.Breakpoint) (*api.Breakpoint, error)
|
||||
// CreateBreakpointWithExpr creates a new breakpoint and sets an expression to restore it after it is disabled.
|
||||
CreateBreakpointWithExpr(*api.Breakpoint, string, [][2]string, bool) (*api.Breakpoint, error)
|
||||
// CreateWatchpoint creates a new watchpoint.
|
||||
CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error)
|
||||
// ListBreakpoints gets all breakpoints.
|
||||
ListBreakpoints(bool) ([]*api.Breakpoint, error)
|
||||
// ClearBreakpoint deletes a breakpoint by ID.
|
||||
ClearBreakpoint(id int) (*api.Breakpoint, error)
|
||||
// ClearBreakpointByName deletes a breakpoint by name
|
||||
ClearBreakpointByName(name string) (*api.Breakpoint, error)
|
||||
// ToggleBreakpoint toggles on or off a breakpoint by ID.
|
||||
ToggleBreakpoint(id int) (*api.Breakpoint, error)
|
||||
// ToggleBreakpointByName toggles on or off a breakpoint by name.
|
||||
ToggleBreakpointByName(name string) (*api.Breakpoint, error)
|
||||
// AmendBreakpoint allows user to update an existing breakpoint for example to change the information
|
||||
// retrieved when the breakpoint is hit or to change, add or remove the break condition
|
||||
AmendBreakpoint(*api.Breakpoint) error
|
||||
// CancelNext cancels a Next or Step call that was interrupted by a manual stop or by another breakpoint
|
||||
CancelNext() error
|
||||
|
||||
// ListThreads lists all threads.
|
||||
ListThreads() ([]*api.Thread, error)
|
||||
// GetThread gets a thread by its ID.
|
||||
GetThread(id int) (*api.Thread, error)
|
||||
|
||||
// ListPackageVariables lists all package variables in the context of the current thread.
|
||||
ListPackageVariables(filter string, cfg api.LoadConfig) ([]api.Variable, error)
|
||||
// EvalVariable returns a variable in the context of the current thread.
|
||||
EvalVariable(scope api.EvalScope, symbol string, cfg api.LoadConfig) (*api.Variable, error)
|
||||
|
||||
// SetVariable sets the value of a variable
|
||||
SetVariable(scope api.EvalScope, symbol, value string) error
|
||||
|
||||
// ListSources lists all source files in the process matching filter.
|
||||
ListSources(filter string) ([]string, error)
|
||||
// ListFunctions lists all functions in the process matching filter.
|
||||
ListFunctions(filter string, tracefollow int) ([]string, error)
|
||||
// ListTypes lists all types in the process matching filter.
|
||||
ListTypes(filter string) ([]string, error)
|
||||
// ListPackagesBuildInfo lists all packages in the process matching filter.
|
||||
ListPackagesBuildInfo(filter string, includeFiles bool) ([]api.PackageBuildInfo, error)
|
||||
// ListLocalVariables lists all local variables in scope.
|
||||
ListLocalVariables(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error)
|
||||
// ListFunctionArgs lists all arguments to the current function.
|
||||
ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error)
|
||||
// ListThreadRegisters lists registers and their values, for the given thread.
|
||||
ListThreadRegisters(threadID int, includeFp bool) (api.Registers, error)
|
||||
// ListScopeRegisters lists registers and their values, for the given scope.
|
||||
ListScopeRegisters(scope api.EvalScope, includeFp bool) (api.Registers, error)
|
||||
|
||||
// ListGoroutines lists all goroutines.
|
||||
ListGoroutines(start, count int) ([]*api.Goroutine, int, error)
|
||||
// ListGoroutinesWithFilter lists goroutines matching the filters
|
||||
ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions, scope *api.EvalScope) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error)
|
||||
|
||||
// Stacktrace returns stacktrace
|
||||
Stacktrace(goroutineID int64, depth int, opts api.StacktraceOptions, cfg *api.LoadConfig) ([]api.Stackframe, error)
|
||||
|
||||
// Ancestors returns ancestor stacktraces
|
||||
Ancestors(goroutineID int64, numAncestors int, depth int) ([]api.Ancestor, error)
|
||||
|
||||
// AttachedToExistingProcess returns whether we attached to a running process or not
|
||||
AttachedToExistingProcess() bool
|
||||
|
||||
// FindLocation returns concrete location information described by a location expression
|
||||
// loc ::= <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
|
||||
// NOTE: this function does not actually set breakpoints.
|
||||
// If findInstruction is true FindLocation will only return locations that correspond to instructions.
|
||||
FindLocation(scope api.EvalScope, loc string, findInstruction bool, substitutePathRules [][2]string) ([]api.Location, string, error)
|
||||
|
||||
// DisassembleRange disassemble code between startPC and endPC
|
||||
DisassembleRange(scope api.EvalScope, startPC, endPC uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error)
|
||||
// DisassemblePC disassemble code of the function containing PC
|
||||
DisassemblePC(scope api.EvalScope, pc uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error)
|
||||
|
||||
// Recorded returns true if the target is a recording.
|
||||
Recorded() bool
|
||||
// TraceDirectory returns the path to the trace directory for a recording.
|
||||
TraceDirectory() (string, error)
|
||||
// Checkpoint sets a checkpoint at the current position.
|
||||
Checkpoint(where string) (checkpointID int, err error)
|
||||
// ListCheckpoints gets all checkpoints.
|
||||
ListCheckpoints() ([]api.Checkpoint, error)
|
||||
// ClearCheckpoint removes a checkpoint
|
||||
ClearCheckpoint(id int) error
|
||||
|
||||
// SetReturnValuesLoadConfig sets the load configuration for return values.
|
||||
SetReturnValuesLoadConfig(*api.LoadConfig)
|
||||
|
||||
// IsMulticlient returns true if the headless instance is multiclient.
|
||||
IsMulticlient() bool
|
||||
|
||||
// ListDynamicLibraries returns a list of loaded dynamic libraries.
|
||||
ListDynamicLibraries() ([]api.Image, error)
|
||||
|
||||
// ExamineMemory returns the raw memory stored at the given address.
|
||||
// The amount of data to be read is specified by length which must be less than or equal to 1000.
|
||||
// This function will return an error if it reads less than `length` bytes.
|
||||
ExamineMemory(address uint64, length int) ([]byte, bool, error)
|
||||
|
||||
// StopRecording stops a recording if one is in progress.
|
||||
StopRecording() error
|
||||
|
||||
// CoreDumpStart starts creating a core dump to the specified file
|
||||
CoreDumpStart(dest string) (api.DumpState, error)
|
||||
// CoreDumpWait waits for the core dump to finish, or for the specified amount of milliseconds
|
||||
CoreDumpWait(msec int) api.DumpState
|
||||
// CoreDumpCancel cancels a core dump in progress
|
||||
CoreDumpCancel() error
|
||||
|
||||
// ListTargets returns the list of connected targets
|
||||
ListTargets() ([]api.Target, error)
|
||||
// FollowExec enables or disables the follow exec mode. In follow exec mode
|
||||
// Delve will automatically debug child processes launched by the target
|
||||
// process
|
||||
FollowExec(bool, string) error
|
||||
FollowExecEnabled() bool
|
||||
|
||||
// Disconnect closes the connection to the server without sending a Detach request first.
|
||||
// If cont is true a continue command will be sent instead.
|
||||
Disconnect(cont bool) error
|
||||
|
||||
// SetDebugInfoDirectories sets directories used to search for debug symbols
|
||||
SetDebugInfoDirectories([]string) error
|
||||
|
||||
// GetDebugInfoDirectories returns the list of directories used to search for debug symbols
|
||||
GetDebugInfoDirectories() ([]string, error)
|
||||
|
||||
// CallAPI allows calling an arbitrary rpc method (used by starlark bindings)
|
||||
CallAPI(method string, args, reply interface{}) error
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/go-delve/delve/service/debugger"
|
||||
)
|
||||
|
||||
// Config provides the configuration to start a Debugger and expose it with a
|
||||
// service.
|
||||
//
|
||||
// Only one of ProcessArgs or AttachPid should be specified. If ProcessArgs is
|
||||
// provided, a new process will be launched. Otherwise, the debugger will try
|
||||
// to attach to an existing process with AttachPid.
|
||||
type Config struct {
|
||||
// Debugger configuration object, used to configure the underlying
|
||||
// debugger used by the server.
|
||||
Debugger debugger.Config
|
||||
|
||||
// Listener is used to serve requests.
|
||||
Listener net.Listener
|
||||
|
||||
// ProcessArgs are the arguments to launch a new process.
|
||||
ProcessArgs []string
|
||||
|
||||
// AcceptMulti configures the server to accept multiple connection.
|
||||
// Note that the server API is not reentrant and clients will have to coordinate.
|
||||
AcceptMulti bool
|
||||
|
||||
// APIVersion selects which version of the API to serve (default: 1).
|
||||
APIVersion int
|
||||
|
||||
// CheckLocalConnUser is true if the debugger should check that local
|
||||
// connections come from the same user that started the headless server
|
||||
CheckLocalConnUser bool
|
||||
|
||||
// DisconnectChan will be closed by the server when the client disconnects
|
||||
DisconnectChan chan<- struct{}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-delve/delve/pkg/config"
|
||||
"github.com/google/go-dap"
|
||||
)
|
||||
|
||||
func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) {
|
||||
vals := strings.SplitN(strings.TrimSpace(cmdstr), " ", 2)
|
||||
cmdname := vals[0]
|
||||
var args string
|
||||
if len(vals) > 1 {
|
||||
args = strings.TrimSpace(vals[1])
|
||||
}
|
||||
for _, cmd := range debugCommands(s) {
|
||||
for _, alias := range cmd.aliases {
|
||||
if alias == cmdname {
|
||||
return cmd.cmdFn(goid, frame, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errNoCmd
|
||||
}
|
||||
|
||||
type cmdfunc func(goid, frame int, args string) (string, error)
|
||||
|
||||
type command struct {
|
||||
aliases []string
|
||||
helpMsg string
|
||||
cmdFn cmdfunc
|
||||
}
|
||||
|
||||
const (
|
||||
msgHelp = `Prints the help message.
|
||||
|
||||
dlv help [command]
|
||||
|
||||
Type "help" followed by the name of a command for more information about it.`
|
||||
|
||||
msgConfig = `Changes configuration parameters.
|
||||
|
||||
dlv config -list
|
||||
|
||||
Show all configuration parameters.
|
||||
|
||||
dlv config -list <parameter>
|
||||
|
||||
Show value of a configuration parameter.
|
||||
|
||||
dlv config <parameter> <value>
|
||||
|
||||
Changes the value of a configuration parameter.
|
||||
|
||||
dlv config substitutePath <from> <to>
|
||||
dlv config substitutePath <from>
|
||||
dlv config substitutePath -clear
|
||||
|
||||
Adds or removes a path substitution rule. If -clear is used all substitutePath rules are removed.
|
||||
See also Documentation/cli/substitutepath.md.
|
||||
|
||||
dlv config showPprofLabels <label>
|
||||
dlv config showPprofLabels -clear <label>
|
||||
dlv config showPprofLabels -clear
|
||||
|
||||
Adds or removes a label key to show in the callstack view. If -clear is used without an argument,
|
||||
all labels are removed.`
|
||||
msgSources = `Print list of source files.
|
||||
|
||||
dlv sources [<regex>]
|
||||
|
||||
If regex is specified only the source files matching it will be returned.`
|
||||
)
|
||||
|
||||
// debugCommands returns a list of commands with default commands defined.
|
||||
func debugCommands(s *Session) []command {
|
||||
return []command{
|
||||
{aliases: []string{"help", "h"}, cmdFn: s.helpMessage, helpMsg: msgHelp},
|
||||
{aliases: []string{"config"}, cmdFn: s.evaluateConfig, helpMsg: msgConfig},
|
||||
{aliases: []string{"sources", "s"}, cmdFn: s.sources, helpMsg: msgSources},
|
||||
}
|
||||
}
|
||||
|
||||
var errNoCmd = errors.New("command not available")
|
||||
|
||||
func (s *Session) helpMessage(_, _ int, args string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if args != "" {
|
||||
for _, cmd := range debugCommands(s) {
|
||||
for _, alias := range cmd.aliases {
|
||||
if alias == args {
|
||||
return cmd.helpMsg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errNoCmd
|
||||
}
|
||||
|
||||
fmt.Fprintln(&buf, "The following commands are available:")
|
||||
|
||||
for _, cmd := range debugCommands(s) {
|
||||
h := cmd.helpMsg
|
||||
if idx := strings.Index(h, "\n"); idx >= 0 {
|
||||
h = h[:idx]
|
||||
}
|
||||
if len(cmd.aliases) > 1 {
|
||||
fmt.Fprintf(&buf, " dlv %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), h)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, " dlv %s \t %s\n", cmd.aliases[0], h)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(&buf)
|
||||
fmt.Fprintln(&buf, "Type 'dlv help' followed by a command for full documentation.")
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
|
||||
argv := config.Split2PartsBySpace(expr)
|
||||
name := argv[0]
|
||||
if name == "-list" {
|
||||
if len(argv) > 1 {
|
||||
return config.ConfigureListByName(&s.args, argv[1], "cfgName"), nil
|
||||
}
|
||||
return listConfig(&s.args), nil
|
||||
}
|
||||
updated, res, err := configureSet(&s.args, expr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if updated {
|
||||
// Send invalidated events for areas that are affected by configuration changes.
|
||||
switch name {
|
||||
case "showGlobalVariables", "showRegisters":
|
||||
// Variable data has become invalidated.
|
||||
s.send(&dap.InvalidatedEvent{
|
||||
Event: *newEvent("invalidated"),
|
||||
Body: dap.InvalidatedEventBody{
|
||||
Areas: []dap.InvalidatedAreas{"variables"},
|
||||
},
|
||||
})
|
||||
case "goroutineFilters", "hideSystemGoroutines", "showPprofLabels":
|
||||
// Thread related data has become invalidated.
|
||||
s.send(&dap.InvalidatedEvent{
|
||||
Event: *newEvent("invalidated"),
|
||||
Body: dap.InvalidatedEventBody{
|
||||
Areas: []dap.InvalidatedAreas{"threads"},
|
||||
},
|
||||
})
|
||||
}
|
||||
res += "\nUpdated"
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Session) sources(_, _ int, filter string) (string, error) {
|
||||
sources, err := s.debugger.Sources(filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sort.Strings(sources)
|
||||
return strings.Join(sources, "\n"), nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-delve/delve/pkg/config"
|
||||
)
|
||||
|
||||
func listConfig(args *launchAttachArgs) string {
|
||||
var buf bytes.Buffer
|
||||
config.ConfigureList(&buf, args, "cfgName")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func configureSet(sargs *launchAttachArgs, args string) (bool, string, error) {
|
||||
v := config.Split2PartsBySpace(args)
|
||||
|
||||
cfgname := v[0]
|
||||
var rest string
|
||||
if len(v) == 2 {
|
||||
rest = v[1]
|
||||
}
|
||||
|
||||
field := config.ConfigureFindFieldByName(sargs, cfgname, "cfgName")
|
||||
if !field.CanAddr() {
|
||||
return false, "", fmt.Errorf("%q is not a configuration parameter", cfgname)
|
||||
}
|
||||
|
||||
if cfgname == "substitutePath" {
|
||||
err := configureSetSubstitutePath(sargs, rest)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
// Print the updated client to server and server to client maps.
|
||||
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
|
||||
}
|
||||
|
||||
if cfgname == "showPprofLabels" {
|
||||
err := configureSetShowPprofLabels(sargs, rest)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
// Print the updated labels
|
||||
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
|
||||
}
|
||||
|
||||
err := config.ConfigureSetSimple(rest, cfgname, field)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
|
||||
}
|
||||
|
||||
func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
|
||||
if strings.TrimSpace(rest) == "-clear" {
|
||||
args.substitutePathClientToServer = args.substitutePathClientToServer[:0]
|
||||
args.substitutePathServerToClient = args.substitutePathServerToClient[:0]
|
||||
return nil
|
||||
}
|
||||
argv := config.SplitQuotedFields(rest, '"')
|
||||
if len(argv) == 2 && argv[0] == "-clear" {
|
||||
argv = argv[1:]
|
||||
}
|
||||
switch len(argv) {
|
||||
case 0:
|
||||
// do nothing, let caller show the current list of substitute path rules
|
||||
return nil
|
||||
case 1: // delete substitute-path rule
|
||||
for i := range args.substitutePathClientToServer {
|
||||
if args.substitutePathClientToServer[i][0] == argv[0] {
|
||||
copy(args.substitutePathClientToServer[i:], args.substitutePathClientToServer[i+1:])
|
||||
args.substitutePathClientToServer = args.substitutePathClientToServer[:len(args.substitutePathClientToServer)-1]
|
||||
copy(args.substitutePathServerToClient[i:], args.substitutePathServerToClient[i+1:])
|
||||
args.substitutePathServerToClient = args.substitutePathServerToClient[:len(args.substitutePathServerToClient)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("could not find rule for %q", argv[0])
|
||||
case 2: // add substitute-path rule
|
||||
for i := range args.substitutePathClientToServer {
|
||||
if args.substitutePathClientToServer[i][0] == argv[0] {
|
||||
args.substitutePathClientToServer[i][1] = argv[1]
|
||||
args.substitutePathServerToClient[i][0] = argv[1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
args.substitutePathClientToServer = append(args.substitutePathClientToServer, [2]string{argv[0], argv[1]})
|
||||
args.substitutePathServerToClient = append(args.substitutePathServerToClient, [2]string{argv[1], argv[0]})
|
||||
|
||||
default:
|
||||
return errors.New("too many arguments to \"config substitutePath\"")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureSetShowPprofLabels(args *launchAttachArgs, rest string) error {
|
||||
if strings.TrimSpace(rest) == "-clear" {
|
||||
args.ShowPprofLabels = args.ShowPprofLabels[:0]
|
||||
return nil
|
||||
}
|
||||
delete := false
|
||||
argv := config.SplitQuotedFields(rest, '"')
|
||||
if len(argv) == 2 && argv[0] == "-clear" {
|
||||
argv = argv[1:]
|
||||
delete = true
|
||||
}
|
||||
switch len(argv) {
|
||||
case 0:
|
||||
// do nothing, let caller show the current list of labels
|
||||
return nil
|
||||
case 1:
|
||||
if delete {
|
||||
for i := range args.ShowPprofLabels {
|
||||
if args.ShowPprofLabels[i] == argv[0] {
|
||||
copy(args.ShowPprofLabels[i:], args.ShowPprofLabels[i+1:])
|
||||
args.ShowPprofLabels = args.ShowPprofLabels[:len(args.ShowPprofLabels)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("could not find label %q", argv[0])
|
||||
} else {
|
||||
args.ShowPprofLabels = append(args.ShowPprofLabels, argv[0])
|
||||
}
|
||||
default:
|
||||
return errors.New("too many arguments to \"config showPprofLabels\"")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListConfig(t *testing.T) {
|
||||
type args struct {
|
||||
args *launchAttachArgs
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
args: &launchAttachArgs{},
|
||||
},
|
||||
want: formatConfig(0, false, false, "", []string{}, false, [][2]string{}),
|
||||
},
|
||||
{
|
||||
name: "default values",
|
||||
args: args{
|
||||
args: &defaultArgs,
|
||||
},
|
||||
want: formatConfig(50, false, false, "", []string{}, false, [][2]string{}),
|
||||
},
|
||||
{
|
||||
name: "custom values",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
StackTraceDepth: 35,
|
||||
ShowGlobalVariables: true,
|
||||
GoroutineFilters: "SomeFilter",
|
||||
ShowPprofLabels: []string{"SomeLabel"},
|
||||
substitutePathClientToServer: [][2]string{{"hello", "world"}},
|
||||
substitutePathServerToClient: [][2]string{{"world", "hello"}},
|
||||
},
|
||||
},
|
||||
want: formatConfig(35, true, false, "SomeFilter", []string{"SomeLabel"}, false, [][2]string{{"hello", "world"}}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := listConfig(tt.args.args); got != tt.want {
|
||||
t.Errorf("listConfig() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureSetSubstitutePath(t *testing.T) {
|
||||
type args struct {
|
||||
args *launchAttachArgs
|
||||
rest string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantRules [][2]string
|
||||
wantErr bool
|
||||
}{
|
||||
// Test add rule.
|
||||
{
|
||||
name: "add rule",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{},
|
||||
substitutePathServerToClient: [][2]string{},
|
||||
},
|
||||
rest: "/path/to/client/dir /path/to/server/dir",
|
||||
},
|
||||
wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "add rule (multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
|
||||
},
|
||||
},
|
||||
rest: "/path/to/client/dir/c /path/to/server/dir/b",
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "add rule from empty string",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{},
|
||||
substitutePathServerToClient: [][2]string{},
|
||||
},
|
||||
rest: `"" /path/to/client/dir`,
|
||||
},
|
||||
wantRules: [][2]string{{"", "/path/to/client/dir"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "add rule to empty string",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{},
|
||||
substitutePathServerToClient: [][2]string{},
|
||||
},
|
||||
rest: `/path/to/client/dir ""`,
|
||||
},
|
||||
wantRules: [][2]string{{"/path/to/client/dir", ""}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "add rule from empty string(multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
|
||||
},
|
||||
},
|
||||
rest: `"" /path/to/client/dir/c`,
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
{"", "/path/to/client/dir/c"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "add rule to empty string(multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
|
||||
},
|
||||
},
|
||||
rest: `/path/to/client/dir/c ""`,
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
{"/path/to/client/dir/c", ""},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
// Test modify rule.
|
||||
{
|
||||
name: "modify rule",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
|
||||
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
|
||||
},
|
||||
rest: "/path/to/client/dir /new/path/to/server/dir",
|
||||
},
|
||||
wantRules: [][2]string{{"/path/to/client/dir", "/new/path/to/server/dir"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "modify rule with from as empty string",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"", "/path/to/server/dir"}},
|
||||
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", ""}},
|
||||
},
|
||||
rest: `"" /new/path/to/server/dir`,
|
||||
},
|
||||
wantRules: [][2]string{{"", "/new/path/to/server/dir"}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "modify rule with to as empty string",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", ""}},
|
||||
substitutePathServerToClient: [][2]string{{"", "/path/to/client/dir"}},
|
||||
},
|
||||
rest: `/path/to/client/dir ""`,
|
||||
},
|
||||
wantRules: [][2]string{{"/path/to/client/dir", ""}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "modify rule (multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/c"},
|
||||
},
|
||||
},
|
||||
rest: "/path/to/client/dir/b /new/path",
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/new/path"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "modify rule with from as empty string(multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"", "/path/to/server/dir/b"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", ""},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/c"},
|
||||
},
|
||||
},
|
||||
rest: `"" /new/path`,
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"", "/new/path"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "modify rule with to as empty string(multiple)",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", "/path/to/server/dir/b"},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
substitutePathServerToClient: [][2]string{
|
||||
{"/path/to/server/dir/a", "/path/to/client/dir/a"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/b"},
|
||||
{"/path/to/server/dir/b", "/path/to/client/dir/c"},
|
||||
},
|
||||
},
|
||||
rest: `/path/to/client/dir/b ""`,
|
||||
},
|
||||
wantRules: [][2]string{
|
||||
{"/path/to/client/dir/a", "/path/to/server/dir/a"},
|
||||
{"/path/to/client/dir/b", ""},
|
||||
{"/path/to/client/dir/c", "/path/to/server/dir/b"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
// Test delete rule.
|
||||
{
|
||||
name: "delete rule",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
|
||||
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
|
||||
},
|
||||
rest: "/path/to/client/dir",
|
||||
},
|
||||
wantRules: [][2]string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "delete rule, empty string",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"", "/path/to/server/dir"}},
|
||||
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", ""}},
|
||||
},
|
||||
rest: `""`,
|
||||
},
|
||||
wantRules: [][2]string{},
|
||||
wantErr: false,
|
||||
},
|
||||
// Test invalid input.
|
||||
{
|
||||
name: "error on delete nonexistent rule",
|
||||
args: args{
|
||||
args: &launchAttachArgs{
|
||||
substitutePathClientToServer: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
|
||||
substitutePathServerToClient: [][2]string{{"/path/to/server/dir", "/path/to/client/dir"}},
|
||||
},
|
||||
rest: "/path/to/server/dir",
|
||||
},
|
||||
wantRules: [][2]string{{"/path/to/client/dir", "/path/to/server/dir"}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := configureSetSubstitutePath(tt.args.args, tt.args.rest)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("configureSetSubstitutePath() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if len(tt.args.args.substitutePathClientToServer) != len(tt.wantRules) {
|
||||
t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer=%v, want %d rules", tt.args.args.substitutePathClientToServer, len(tt.wantRules))
|
||||
return
|
||||
}
|
||||
gotClient2Server := tt.args.args.substitutePathClientToServer
|
||||
gotServer2Client := tt.args.args.substitutePathServerToClient
|
||||
for i, rule := range tt.wantRules {
|
||||
if gotClient2Server[i][0] != rule[0] || gotClient2Server[i][1] != rule[1] {
|
||||
t.Errorf("configureSetSubstitutePath() got substitutePathClientToServer[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], rule)
|
||||
}
|
||||
if gotServer2Client[i][1] != rule[0] || gotServer2Client[i][0] != rule[1] {
|
||||
reverseRule := [2]string{rule[1], rule[0]}
|
||||
t.Errorf("configureSetSubstitutePath() got substitutePathServerToClient[%d]=%#v,\n want %#v rules", i, gotClient2Server[i], reverseRule)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
// Package daptest provides a sample client with utilities
|
||||
// for DAP mode testing.
|
||||
package daptest
|
||||
|
||||
//go:generate go run ./gen/main.go -o ./resp.go
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
)
|
||||
|
||||
// Client is a debugger service client that uses Debug Adaptor Protocol.
|
||||
// It does not (yet?) implement service.Client interface.
|
||||
// All client methods are synchronous.
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
// seq is used to track the sequence number of each
|
||||
// requests that the client sends to the server
|
||||
seq int
|
||||
}
|
||||
|
||||
// NewClient creates a new Client over a TCP connection.
|
||||
// Call Close() to close the connection.
|
||||
func NewClient(addr string) *Client {
|
||||
fmt.Println("Connecting to server at:", addr)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
return NewClientFromConn(conn)
|
||||
}
|
||||
|
||||
// NewClientFromConn creates a new Client with the given TCP connection.
|
||||
// Call Close to close the connection.
|
||||
func NewClientFromConn(conn net.Conn) *Client {
|
||||
c := &Client{conn: conn, reader: bufio.NewReader(conn)}
|
||||
c.seq = 1 // match VS Code numbering
|
||||
return c
|
||||
}
|
||||
|
||||
// Close closes the client connection.
|
||||
func (c *Client) Close() {
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) send(request dap.Message) {
|
||||
dap.WriteProtocolMessage(c.conn, request)
|
||||
}
|
||||
|
||||
func (c *Client) ReadMessage() (dap.Message, error) {
|
||||
return dap.ReadProtocolMessage(c.reader)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectMessage(t *testing.T) dap.Message {
|
||||
t.Helper()
|
||||
m, err := dap.ReadProtocolMessage(c.reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *Client) ExpectInvisibleErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||
t.Helper()
|
||||
er := c.ExpectErrorResponse(t)
|
||||
if er.Body.Error != nil && er.Body.Error.ShowUser {
|
||||
t.Errorf("\ngot %#v\nwant ShowUser=false", er)
|
||||
}
|
||||
return er
|
||||
}
|
||||
|
||||
func (c *Client) ExpectVisibleErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||
t.Helper()
|
||||
er := c.ExpectErrorResponse(t)
|
||||
if er.Body.Error == nil || !er.Body.Error.ShowUser {
|
||||
t.Errorf("\ngot %#v\nwant ShowUser=true", er)
|
||||
}
|
||||
return er
|
||||
}
|
||||
|
||||
func (c *Client) ExpectErrorResponseWith(t *testing.T, id int, message string, showUser bool) *dap.ErrorResponse {
|
||||
t.Helper()
|
||||
er := c.ExpectErrorResponse(t)
|
||||
if er.Body.Error == nil {
|
||||
t.Errorf("got nil, want Id=%d Format=%q ShowUser=%v", id, message, showUser)
|
||||
return er
|
||||
}
|
||||
if matched, _ := regexp.MatchString(message, er.Body.Error.Format); !matched || er.Body.Error.Id != id || er.Body.Error.ShowUser != showUser {
|
||||
t.Errorf("got %#v, want Id=%d Format=%q ShowUser=%v", er, id, message, showUser)
|
||||
}
|
||||
return er
|
||||
}
|
||||
|
||||
func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.InitializeResponse {
|
||||
t.Helper()
|
||||
initResp := c.ExpectInitializeResponse(t)
|
||||
wantCapabilities := dap.Capabilities{
|
||||
// the values set by dap.(*Server).onInitializeRequest.
|
||||
SupportsConfigurationDoneRequest: true,
|
||||
SupportsConditionalBreakpoints: true,
|
||||
SupportsDelayedStackTraceLoading: true,
|
||||
SupportsExceptionInfoRequest: true,
|
||||
SupportsSetVariable: true,
|
||||
SupportsFunctionBreakpoints: true,
|
||||
SupportsInstructionBreakpoints: true,
|
||||
SupportsEvaluateForHovers: true,
|
||||
SupportsClipboardContext: true,
|
||||
SupportsSteppingGranularity: true,
|
||||
SupportsLogPoints: true,
|
||||
SupportsDisassembleRequest: true,
|
||||
}
|
||||
if !reflect.DeepEqual(initResp.Body, wantCapabilities) {
|
||||
t.Errorf("capabilities in initializeResponse: got %+v, want %v", pretty(initResp.Body), pretty(wantCapabilities))
|
||||
}
|
||||
return initResp
|
||||
}
|
||||
|
||||
func pretty(v interface{}) string {
|
||||
s, _ := json.MarshalIndent(v, "", "\t")
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectNotYetImplementedErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||
t.Helper()
|
||||
return c.ExpectErrorResponseWith(t, 7777, "Not yet implemented", false)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectUnsupportedCommandErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||
t.Helper()
|
||||
return c.ExpectErrorResponseWith(t, 9999, "Unsupported command", false)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectCapabilitiesEventSupportTerminateDebuggee(t *testing.T) *dap.CapabilitiesEvent {
|
||||
t.Helper()
|
||||
e := c.ExpectCapabilitiesEvent(t)
|
||||
if !e.Body.Capabilities.SupportTerminateDebuggee {
|
||||
t.Errorf("\ngot %#v\nwant SupportTerminateDebuggee=true", e.Body.Capabilities.SupportTerminateDebuggee)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
e := c.ExpectOutputEvent(t)
|
||||
if matched, _ := regexp.MatchString(want, e.Body.Output); !matched {
|
||||
t.Errorf("\ngot %#v\nwant Output=%q", e, want)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
const ProcessExited = `Process [0-9]+ has exited with status %s\n`
|
||||
|
||||
func (c *Client) ExpectOutputEventProcessExited(t *testing.T, status int) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
// We sometimes fail to return the correct exit status on Linux, so allow -1 here as well.
|
||||
return c.ExpectOutputEventRegex(t, fmt.Sprintf(ProcessExited, fmt.Sprintf("(%d|-1)", status)))
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventProcessExitedAnyStatus(t *testing.T) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, fmt.Sprintf(ProcessExited, `-?\d+`))
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventDetaching(t *testing.T) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, `Detaching\n`)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventDetachingKill(t *testing.T) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, `Detaching and terminating target process\n`)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventDetachingNoKill(t *testing.T) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, `Detaching without terminating target process\n`)
|
||||
}
|
||||
|
||||
func (c *Client) ExpectOutputEventTerminating(t *testing.T) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, `Terminating process [0-9]+\n`)
|
||||
}
|
||||
|
||||
const ClosingClient = "Closing client session, but leaving multi-client DAP server at .+:[0-9]+ with debuggee %s\n"
|
||||
|
||||
func (c *Client) ExpectOutputEventClosingClient(t *testing.T, status string) *dap.OutputEvent {
|
||||
t.Helper()
|
||||
return c.ExpectOutputEventRegex(t, fmt.Sprintf(ClosingClient, status))
|
||||
}
|
||||
|
||||
func (c *Client) CheckStopLocation(t *testing.T, thread int, name string, line interface{}) {
|
||||
t.Helper()
|
||||
c.StackTraceRequest(thread, 0, 20)
|
||||
st := c.ExpectStackTraceResponse(t)
|
||||
if len(st.Body.StackFrames) < 1 {
|
||||
t.Errorf("\ngot %#v\nwant len(stackframes) => 1", st)
|
||||
} else {
|
||||
switch line := line.(type) {
|
||||
case int:
|
||||
if line != -1 && st.Body.StackFrames[0].Line != line {
|
||||
t.Errorf("\ngot %#v\nwant Line=%d", st, line)
|
||||
}
|
||||
case []int:
|
||||
found := false
|
||||
for _, line := range line {
|
||||
if st.Body.StackFrames[0].Line == line {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("\ngot %#v\nwant Line=%v", st, line)
|
||||
}
|
||||
}
|
||||
if st.Body.StackFrames[0].Name != name {
|
||||
t.Errorf("\ngot %#v\nwant Name=%q", st, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeRequest sends an 'initialize' request.
|
||||
func (c *Client) InitializeRequest() {
|
||||
request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
|
||||
request.Arguments = dap.InitializeRequestArguments{
|
||||
AdapterID: "go",
|
||||
PathFormat: "path",
|
||||
LinesStartAt1: true,
|
||||
ColumnsStartAt1: true,
|
||||
SupportsVariableType: true,
|
||||
SupportsVariablePaging: true,
|
||||
SupportsRunInTerminalRequest: true,
|
||||
Locale: "en-us",
|
||||
}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// InitializeRequestWithArgs sends an 'initialize' request with specified arguments.
|
||||
func (c *Client) InitializeRequestWithArgs(args dap.InitializeRequestArguments) {
|
||||
request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
|
||||
request.Arguments = args
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
func toRawMessage(in interface{}) json.RawMessage {
|
||||
out, _ := json.Marshal(in)
|
||||
return out
|
||||
}
|
||||
|
||||
// LaunchRequest sends a 'launch' request with the specified args.
|
||||
func (c *Client) LaunchRequest(mode, program string, stopOnEntry bool) {
|
||||
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
|
||||
request.Arguments = toRawMessage(map[string]interface{}{
|
||||
"request": "launch",
|
||||
"mode": mode,
|
||||
"program": program,
|
||||
"stopOnEntry": stopOnEntry,
|
||||
})
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// LaunchRequestWithArgs takes a map of untyped implementation-specific
|
||||
// arguments to send a 'launch' request. This version can be used to
|
||||
// test for values of unexpected types or unspecified values.
|
||||
func (c *Client) LaunchRequestWithArgs(arguments map[string]interface{}) {
|
||||
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
|
||||
request.Arguments = toRawMessage(arguments)
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// AttachRequest sends an 'attach' request with the specified
|
||||
// arguments.
|
||||
func (c *Client) AttachRequest(arguments map[string]interface{}) {
|
||||
request := &dap.AttachRequest{Request: *c.newRequest("attach")}
|
||||
request.Arguments = toRawMessage(arguments)
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// DisconnectRequest sends a 'disconnect' request.
|
||||
func (c *Client) DisconnectRequest() {
|
||||
request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// DisconnectRequestWithKillOption sends a 'disconnect' request with an option to specify
|
||||
// `terminateDebuggee`.
|
||||
func (c *Client) DisconnectRequestWithKillOption(kill bool) {
|
||||
request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")}
|
||||
request.Arguments = &dap.DisconnectArguments{
|
||||
TerminateDebuggee: kill,
|
||||
}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// SetBreakpointsRequest sends a 'setBreakpoints' request.
|
||||
func (c *Client) SetBreakpointsRequest(file string, lines []int) {
|
||||
c.SetBreakpointsRequestWithArgs(file, lines, nil, nil, nil)
|
||||
}
|
||||
|
||||
// SetBreakpointsRequestWithArgs sends a 'setBreakpoints' request with an option to
|
||||
// specify conditions, hit conditions, and log messages.
|
||||
func (c *Client) SetBreakpointsRequestWithArgs(file string, lines []int, conditions, hitConditions, logMessages map[int]string) {
|
||||
request := &dap.SetBreakpointsRequest{Request: *c.newRequest("setBreakpoints")}
|
||||
request.Arguments = dap.SetBreakpointsArguments{
|
||||
Source: dap.Source{
|
||||
Name: filepath.Base(file),
|
||||
Path: file,
|
||||
},
|
||||
Breakpoints: make([]dap.SourceBreakpoint, len(lines)),
|
||||
}
|
||||
for i, l := range lines {
|
||||
request.Arguments.Breakpoints[i].Line = l
|
||||
if cond, ok := conditions[l]; ok {
|
||||
request.Arguments.Breakpoints[i].Condition = cond
|
||||
}
|
||||
if hitCond, ok := hitConditions[l]; ok {
|
||||
request.Arguments.Breakpoints[i].HitCondition = hitCond
|
||||
}
|
||||
if logMessage, ok := logMessages[l]; ok {
|
||||
request.Arguments.Breakpoints[i].LogMessage = logMessage
|
||||
}
|
||||
}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// SetExceptionBreakpointsRequest sends a 'setExceptionBreakpoints' request.
|
||||
func (c *Client) SetExceptionBreakpointsRequest() {
|
||||
request := &dap.SetBreakpointsRequest{Request: *c.newRequest("setExceptionBreakpoints")}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// ConfigurationDoneRequest sends a 'configurationDone' request.
|
||||
func (c *Client) ConfigurationDoneRequest() {
|
||||
request := &dap.ConfigurationDoneRequest{Request: *c.newRequest("configurationDone")}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// ContinueRequest sends a 'continue' request.
|
||||
func (c *Client) ContinueRequest(thread int) {
|
||||
request := &dap.ContinueRequest{Request: *c.newRequest("continue")}
|
||||
request.Arguments.ThreadId = thread
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// NextRequest sends a 'next' request.
|
||||
func (c *Client) NextRequest(thread int) {
|
||||
request := &dap.NextRequest{Request: *c.newRequest("next")}
|
||||
request.Arguments.ThreadId = thread
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// NextInstructionRequest sends a 'next' request with granularity 'instruction'.
|
||||
func (c *Client) NextInstructionRequest(thread int) {
|
||||
request := &dap.NextRequest{Request: *c.newRequest("next")}
|
||||
request.Arguments.ThreadId = thread
|
||||
request.Arguments.Granularity = "instruction"
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StepInRequest sends a 'stepIn' request.
|
||||
func (c *Client) StepInRequest(thread int) {
|
||||
request := &dap.StepInRequest{Request: *c.newRequest("stepIn")}
|
||||
request.Arguments.ThreadId = thread
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StepInInstructionRequest sends a 'stepIn' request with granularity 'instruction'.
|
||||
func (c *Client) StepInInstructionRequest(thread int) {
|
||||
request := &dap.StepInRequest{Request: *c.newRequest("stepIn")}
|
||||
request.Arguments.ThreadId = thread
|
||||
request.Arguments.Granularity = "instruction"
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StepOutRequest sends a 'stepOut' request.
|
||||
func (c *Client) StepOutRequest(thread int) {
|
||||
request := &dap.StepOutRequest{Request: *c.newRequest("stepOut")}
|
||||
request.Arguments.ThreadId = thread
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StepOutInstructionRequest sends a 'stepOut' request with granularity 'instruction'.
|
||||
func (c *Client) StepOutInstructionRequest(thread int) {
|
||||
request := &dap.StepOutRequest{Request: *c.newRequest("stepOut")}
|
||||
request.Arguments.ThreadId = thread
|
||||
request.Arguments.Granularity = "instruction"
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// PauseRequest sends a 'pause' request.
|
||||
func (c *Client) PauseRequest(threadId int) {
|
||||
request := &dap.PauseRequest{Request: *c.newRequest("pause")}
|
||||
request.Arguments.ThreadId = threadId
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// ThreadsRequest sends a 'threads' request.
|
||||
func (c *Client) ThreadsRequest() {
|
||||
request := &dap.ThreadsRequest{Request: *c.newRequest("threads")}
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StackTraceRequest sends a 'stackTrace' request.
|
||||
func (c *Client) StackTraceRequest(threadID, startFrame, levels int) {
|
||||
request := &dap.StackTraceRequest{Request: *c.newRequest("stackTrace")}
|
||||
request.Arguments.ThreadId = threadID
|
||||
request.Arguments.StartFrame = startFrame
|
||||
request.Arguments.Levels = levels
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// ScopesRequest sends a 'scopes' request.
|
||||
func (c *Client) ScopesRequest(frameID int) {
|
||||
request := &dap.ScopesRequest{Request: *c.newRequest("scopes")}
|
||||
request.Arguments.FrameId = frameID
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// VariablesRequest sends a 'variables' request.
|
||||
func (c *Client) VariablesRequest(variablesReference int) {
|
||||
request := &dap.VariablesRequest{Request: *c.newRequest("variables")}
|
||||
request.Arguments.VariablesReference = variablesReference
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// IndexedVariablesRequest sends a 'variables' request.
|
||||
func (c *Client) IndexedVariablesRequest(variablesReference, start, count int) {
|
||||
request := &dap.VariablesRequest{Request: *c.newRequest("variables")}
|
||||
request.Arguments.VariablesReference = variablesReference
|
||||
request.Arguments.Filter = "indexed"
|
||||
request.Arguments.Start = start
|
||||
request.Arguments.Count = count
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// NamedVariablesRequest sends a 'variables' request.
|
||||
func (c *Client) NamedVariablesRequest(variablesReference int) {
|
||||
request := &dap.VariablesRequest{Request: *c.newRequest("variables")}
|
||||
request.Arguments.VariablesReference = variablesReference
|
||||
request.Arguments.Filter = "named"
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// TerminateRequest sends a 'terminate' request.
|
||||
func (c *Client) TerminateRequest() {
|
||||
c.send(&dap.TerminateRequest{Request: *c.newRequest("terminate")})
|
||||
}
|
||||
|
||||
// RestartRequest sends a 'restart' request.
|
||||
func (c *Client) RestartRequest() {
|
||||
c.send(&dap.RestartRequest{Request: *c.newRequest("restart")})
|
||||
}
|
||||
|
||||
// SetFunctionBreakpointsRequest sends a 'setFunctionBreakpoints' request.
|
||||
func (c *Client) SetFunctionBreakpointsRequest(breakpoints []dap.FunctionBreakpoint) {
|
||||
c.send(&dap.SetFunctionBreakpointsRequest{
|
||||
Request: *c.newRequest("setFunctionBreakpoints"),
|
||||
Arguments: dap.SetFunctionBreakpointsArguments{
|
||||
Breakpoints: breakpoints,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SetInstructionBreakpointsRequest sends a 'setInstructionBreakpoints' request.
|
||||
func (c *Client) SetInstructionBreakpointsRequest(breakpoints []dap.InstructionBreakpoint) {
|
||||
c.send(&dap.SetInstructionBreakpointsRequest{
|
||||
Request: *c.newRequest("setInstructionBreakpoints"),
|
||||
Arguments: dap.SetInstructionBreakpointsArguments{
|
||||
Breakpoints: breakpoints,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// StepBackRequest sends a 'stepBack' request.
|
||||
func (c *Client) StepBackRequest() {
|
||||
c.send(&dap.StepBackRequest{Request: *c.newRequest("stepBack")})
|
||||
}
|
||||
|
||||
// ReverseContinueRequest sends a 'reverseContinue' request.
|
||||
func (c *Client) ReverseContinueRequest() {
|
||||
c.send(&dap.ReverseContinueRequest{Request: *c.newRequest("reverseContinue")})
|
||||
}
|
||||
|
||||
// SetVariableRequest sends a 'setVariable' request.
|
||||
func (c *Client) SetVariableRequest(variablesRef int, name, value string) {
|
||||
request := &dap.SetVariableRequest{Request: *c.newRequest("setVariable")}
|
||||
request.Arguments.VariablesReference = variablesRef
|
||||
request.Arguments.Name = name
|
||||
request.Arguments.Value = value
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// RestartFrameRequest sends a 'restartFrame' request.
|
||||
func (c *Client) RestartFrameRequest() {
|
||||
c.send(&dap.RestartFrameRequest{Request: *c.newRequest("restartFrame")})
|
||||
}
|
||||
|
||||
// GotoRequest sends a 'goto' request.
|
||||
func (c *Client) GotoRequest() {
|
||||
c.send(&dap.GotoRequest{Request: *c.newRequest("goto")})
|
||||
}
|
||||
|
||||
// SetExpressionRequest sends a 'setExpression' request.
|
||||
func (c *Client) SetExpressionRequest() {
|
||||
c.send(&dap.SetExpressionRequest{Request: *c.newRequest("setExpression")})
|
||||
}
|
||||
|
||||
// SourceRequest sends a 'source' request.
|
||||
func (c *Client) SourceRequest() {
|
||||
c.send(&dap.SourceRequest{Request: *c.newRequest("source")})
|
||||
}
|
||||
|
||||
// TerminateThreadsRequest sends a 'terminateThreads' request.
|
||||
func (c *Client) TerminateThreadsRequest() {
|
||||
c.send(&dap.TerminateThreadsRequest{Request: *c.newRequest("terminateThreads")})
|
||||
}
|
||||
|
||||
// EvaluateRequest sends a 'evaluate' request.
|
||||
func (c *Client) EvaluateRequest(expr string, fid int, context string) {
|
||||
request := &dap.EvaluateRequest{Request: *c.newRequest("evaluate")}
|
||||
request.Arguments.Expression = expr
|
||||
request.Arguments.FrameId = fid
|
||||
request.Arguments.Context = context
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// StepInTargetsRequest sends a 'stepInTargets' request.
|
||||
func (c *Client) StepInTargetsRequest() {
|
||||
c.send(&dap.StepInTargetsRequest{Request: *c.newRequest("stepInTargets")})
|
||||
}
|
||||
|
||||
// GotoTargetsRequest sends a 'gotoTargets' request.
|
||||
func (c *Client) GotoTargetsRequest() {
|
||||
c.send(&dap.GotoTargetsRequest{Request: *c.newRequest("gotoTargets")})
|
||||
}
|
||||
|
||||
// CompletionsRequest sends a 'completions' request.
|
||||
func (c *Client) CompletionsRequest() {
|
||||
c.send(&dap.CompletionsRequest{Request: *c.newRequest("completions")})
|
||||
}
|
||||
|
||||
// ExceptionInfoRequest sends a 'exceptionInfo' request.
|
||||
func (c *Client) ExceptionInfoRequest(threadID int) {
|
||||
request := &dap.ExceptionInfoRequest{Request: *c.newRequest("exceptionInfo")}
|
||||
request.Arguments.ThreadId = threadID
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// LoadedSourcesRequest sends a 'loadedSources' request.
|
||||
func (c *Client) LoadedSourcesRequest() {
|
||||
c.send(&dap.LoadedSourcesRequest{Request: *c.newRequest("loadedSources")})
|
||||
}
|
||||
|
||||
// DataBreakpointInfoRequest sends a 'dataBreakpointInfo' request.
|
||||
func (c *Client) DataBreakpointInfoRequest() {
|
||||
c.send(&dap.DataBreakpointInfoRequest{Request: *c.newRequest("dataBreakpointInfo")})
|
||||
}
|
||||
|
||||
// SetDataBreakpointsRequest sends a 'setDataBreakpoints' request.
|
||||
func (c *Client) SetDataBreakpointsRequest() {
|
||||
c.send(&dap.SetDataBreakpointsRequest{Request: *c.newRequest("setDataBreakpoints")})
|
||||
}
|
||||
|
||||
// ReadMemoryRequest sends a 'readMemory' request.
|
||||
func (c *Client) ReadMemoryRequest() {
|
||||
c.send(&dap.ReadMemoryRequest{Request: *c.newRequest("readMemory")})
|
||||
}
|
||||
|
||||
// DisassembleRequest sends a 'disassemble' request.
|
||||
func (c *Client) DisassembleRequest(memoryReference string, instructionOffset, instructionCount int) {
|
||||
c.send(&dap.DisassembleRequest{
|
||||
Request: *c.newRequest("disassemble"),
|
||||
Arguments: dap.DisassembleArguments{
|
||||
MemoryReference: memoryReference,
|
||||
Offset: 0,
|
||||
InstructionOffset: instructionOffset,
|
||||
InstructionCount: instructionCount,
|
||||
ResolveSymbols: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// CancelRequest sends a 'cancel' request.
|
||||
func (c *Client) CancelRequest() {
|
||||
c.send(&dap.CancelRequest{Request: *c.newRequest("cancel")})
|
||||
}
|
||||
|
||||
// BreakpointLocationsRequest sends a 'breakpointLocations' request.
|
||||
func (c *Client) BreakpointLocationsRequest() {
|
||||
c.send(&dap.BreakpointLocationsRequest{Request: *c.newRequest("breakpointLocations")})
|
||||
}
|
||||
|
||||
// ModulesRequest sends a 'modules' request.
|
||||
func (c *Client) ModulesRequest() {
|
||||
c.send(&dap.ModulesRequest{Request: *c.newRequest("modules")})
|
||||
}
|
||||
|
||||
// UnknownRequest triggers dap.DecodeProtocolMessageFieldError.
|
||||
func (c *Client) UnknownRequest() {
|
||||
request := c.newRequest("unknown")
|
||||
c.send(request)
|
||||
}
|
||||
|
||||
// UnknownEvent triggers dap.DecodeProtocolMessageFieldError.
|
||||
func (c *Client) UnknownEvent() {
|
||||
event := &dap.Event{}
|
||||
event.Type = "event"
|
||||
event.Seq = -1
|
||||
event.Event = "unknown"
|
||||
c.send(event)
|
||||
}
|
||||
|
||||
// BadRequest triggers an unmarshal error.
|
||||
func (c *Client) BadRequest() {
|
||||
content := []byte("{malformedString}")
|
||||
contentLengthHeaderFmt := "Content-Length: %d\r\n\r\n"
|
||||
header := fmt.Sprintf(contentLengthHeaderFmt, len(content))
|
||||
c.conn.Write([]byte(header))
|
||||
c.conn.Write(content)
|
||||
}
|
||||
|
||||
// KnownEvent passes decode checks, but delve has no 'case' to
|
||||
// handle it. This behaves the same way a new request type
|
||||
// added to go-dap, but not to delve.
|
||||
func (c *Client) KnownEvent() {
|
||||
event := &dap.Event{}
|
||||
event.Type = "event"
|
||||
event.Seq = -1
|
||||
event.Event = "terminated"
|
||||
c.send(event)
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(command string) *dap.Request {
|
||||
request := &dap.Request{}
|
||||
request.Type = "request"
|
||||
request.Command = command
|
||||
request.Seq = c.seq
|
||||
c.seq++
|
||||
return request
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Binary gen generates service/dap/daptest/responses.go.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"go/types"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
|
||||
_ "github.com/google/go-dap"
|
||||
)
|
||||
|
||||
var oFlag = flag.String("o", "", "output file name")
|
||||
|
||||
var tmpl = template.Must(template.New("assert").Parse(`package daptest
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// The code generator program is in ./gen directory.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
)
|
||||
{{range .}}
|
||||
// Expect{{.}} reads a protocol message from the connection
|
||||
// and fails the test if the read message is not *{{.}}.
|
||||
func (c *Client) Expect{{.}}(t *testing.T) *dap.{{.}} {
|
||||
t.Helper()
|
||||
m := c.ExpectMessage(t)
|
||||
return c.Check{{.}}(t, m)
|
||||
}
|
||||
|
||||
// Check{{.}} fails the test if m is not *{{.}}.
|
||||
func (c *Client) Check{{.}}(t *testing.T, m dap.Message) *dap.{{.}} {
|
||||
t.Helper(){{if or (or (eq . "StepInResponse") (eq . "StepOutResponse")) (eq . "NextResponse") }}
|
||||
_, ok := m.(*dap.ContinuedEvent)
|
||||
if !ok {
|
||||
t.Fatalf("got %#v, want *dap.ContinuedEvent", m)
|
||||
}
|
||||
m = c.ExpectMessage(t){{else}}{{if (eq . "ConfigurationDoneResponse") }}
|
||||
oe, ok := m.(*dap.OutputEvent)
|
||||
if !ok {
|
||||
t.Fatalf("got %#v, want *dap.OutputEvent", m)
|
||||
}
|
||||
if oe.Body.Output != "Type 'dlv help' for list of commands.\n" {
|
||||
t.Fatalf("got %#v, want Output=%q", m, "Type 'dlv help' for list of commands.\n")
|
||||
}
|
||||
m = c.ExpectMessage(t){{end}}{{end}}
|
||||
r, ok := m.(*dap.{{.}})
|
||||
if !ok {
|
||||
t.Fatalf("got %#v, want *dap.{{.}}", m)
|
||||
}
|
||||
return r
|
||||
}{{end}}
|
||||
`))
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *oFlag == "" {
|
||||
fmt.Fprintf(os.Stderr, "-o must be provided\n")
|
||||
}
|
||||
|
||||
pkgs, err := packages.Load(&packages.Config{
|
||||
Mode: packages.NeedTypes,
|
||||
}, "github.com/google/go-dap")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "load: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(pkgs) != 1 || pkgs[0].Types == nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid package was loaded: %#v\n", pkgs)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
messages := []string{}
|
||||
scope := pkgs[0].Types.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
// Find only types that are embedding go-dap.Response message.
|
||||
obj := scope.Lookup(name)
|
||||
if !obj.Exported() {
|
||||
continue // skip unexported
|
||||
}
|
||||
u, ok := obj.Type().Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i := 0; i < u.NumFields(); i++ {
|
||||
if f := u.Field(i); f.Embedded() && (f.Type().String() == "github.com/google/go-dap.Response" ||
|
||||
f.Type().String() == "github.com/google/go-dap.Event") {
|
||||
messages = append(messages, obj.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := tmpl.Execute(buf, messages); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
formatted, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Generated invalid go code: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.WriteFile(*oFlag, formatted, 0o644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
package dap
|
||||
|
||||
// Unique identifiers for messages returned for errors from requests.
|
||||
// These values are not mandated by DAP (other than the uniqueness
|
||||
// requirement), so each implementation is free to choose their own.
|
||||
const (
|
||||
UnsupportedCommand int = 9999
|
||||
InternalError int = 8888
|
||||
NotYetImplemented int = 7777
|
||||
|
||||
// Where applicable and for consistency only,
|
||||
// values below are inspired the original vscode-go debug adaptor.
|
||||
|
||||
FailedToLaunch = 3000
|
||||
FailedToAttach = 3001
|
||||
FailedToInitialize = 3002
|
||||
UnableToSetBreakpoints = 2002
|
||||
UnableToDisplayThreads = 2003
|
||||
UnableToProduceStackTrace = 2004
|
||||
UnableToListLocals = 2005
|
||||
UnableToListArgs = 2006
|
||||
UnableToListGlobals = 2007
|
||||
UnableToLookupVariable = 2008
|
||||
UnableToEvaluateExpression = 2009
|
||||
UnableToHalt = 2010
|
||||
UnableToGetExceptionInfo = 2011
|
||||
UnableToSetVariable = 2012
|
||||
UnableToDisassemble = 2013
|
||||
UnableToListRegisters = 2014
|
||||
UnableToRunDlvCommand = 2015
|
||||
|
||||
// Add more codes as we support more requests
|
||||
|
||||
NoDebugIsRunning = 3000
|
||||
DebuggeeIsRunning = 4000
|
||||
DisconnectError = 5000
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
package dap
|
||||
|
||||
import "github.com/go-delve/delve/pkg/proc"
|
||||
|
||||
const startHandle = 1000
|
||||
|
||||
// handlesMap maps arbitrary values to unique sequential ids.
|
||||
// This provides convenient abstraction of references, offering
|
||||
// opacity and allowing simplification of complex identifiers.
|
||||
// Based on
|
||||
// https://github.com/microsoft/vscode-debugadapter-node/blob/master/adapter/src/handles.ts
|
||||
type handlesMap struct {
|
||||
nextHandle int
|
||||
handleToVal map[int]interface{}
|
||||
}
|
||||
|
||||
type fullyQualifiedVariable struct {
|
||||
*proc.Variable
|
||||
// A way to load this variable by either using all names in the hierarchic
|
||||
// sequence above this variable (most readable when referenced by the UI)
|
||||
// if available or a special expression based on:
|
||||
// https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#loading-more-of-a-variable
|
||||
// Empty if the variable cannot or should not be reloaded.
|
||||
fullyQualifiedNameOrExpr string
|
||||
// True if this represents variable scope
|
||||
isScope bool
|
||||
// startIndex is the index of the first child for an array or slice.
|
||||
// This variable represents a chunk of the array, slice or map.
|
||||
startIndex int
|
||||
}
|
||||
|
||||
func newHandlesMap() *handlesMap {
|
||||
return &handlesMap{startHandle, make(map[int]interface{})}
|
||||
}
|
||||
|
||||
func (hs *handlesMap) reset() {
|
||||
hs.nextHandle = startHandle
|
||||
hs.handleToVal = make(map[int]interface{})
|
||||
}
|
||||
|
||||
func (hs *handlesMap) create(value interface{}) int {
|
||||
next := hs.nextHandle
|
||||
hs.nextHandle++
|
||||
hs.handleToVal[next] = value
|
||||
return next
|
||||
}
|
||||
|
||||
func (hs *handlesMap) get(handle int) (interface{}, bool) {
|
||||
v, ok := hs.handleToVal[handle]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
type variablesHandlesMap struct {
|
||||
m *handlesMap
|
||||
}
|
||||
|
||||
func newVariablesHandlesMap() *variablesHandlesMap {
|
||||
return &variablesHandlesMap{newHandlesMap()}
|
||||
}
|
||||
|
||||
func (hs *variablesHandlesMap) create(value *fullyQualifiedVariable) int {
|
||||
return hs.m.create(value)
|
||||
}
|
||||
|
||||
func (hs *variablesHandlesMap) get(handle int) (*fullyQualifiedVariable, bool) {
|
||||
v, ok := hs.m.get(handle)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return v.(*fullyQualifiedVariable), true
|
||||
}
|
||||
|
||||
func (hs *variablesHandlesMap) reset() {
|
||||
hs.m.reset()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Launch debug sessions support the following modes:
|
||||
//
|
||||
// -- [DEFAULT] "debug" - builds and launches debugger for specified program (similar to 'dlv debug')
|
||||
//
|
||||
// Required args: program
|
||||
// Optional args with default: output, cwd, noDebug
|
||||
// Optional args: buildFlags, args
|
||||
//
|
||||
// -- "test" - builds and launches debugger for specified test (similar to 'dlv test')
|
||||
//
|
||||
// same args as above
|
||||
//
|
||||
// -- "exec" - launches debugger for precompiled binary (similar to 'dlv exec')
|
||||
//
|
||||
// Required args: program
|
||||
// Optional args with default: cwd, noDebug
|
||||
// Optional args: args
|
||||
//
|
||||
// -- "replay" - replays a trace generated by mozilla rr. Mozilla rr must be installed.
|
||||
//
|
||||
// Required args: traceDirPath
|
||||
// Optional args: args
|
||||
//
|
||||
// -- "core" - examines a core dump (only supports linux and windows core dumps).
|
||||
//
|
||||
// Required args: program, coreFilePath
|
||||
// Optional args: args
|
||||
//
|
||||
// TODO(hyangah): change this to 'validateLaunchMode' that checks
|
||||
// all the required/optional fields mentioned above.
|
||||
func isValidLaunchMode(mode string) bool {
|
||||
switch mode {
|
||||
case "exec", "debug", "test", "replay", "core":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Default values for Launch/Attach configs.
|
||||
// Used to initialize configuration variables before decoding
|
||||
// arguments in launch/attach requests.
|
||||
var (
|
||||
defaultLaunchAttachCommonConfig = LaunchAttachCommonConfig{
|
||||
Backend: "default",
|
||||
StackTraceDepth: 50,
|
||||
}
|
||||
defaultLaunchConfig = LaunchConfig{
|
||||
Mode: "debug",
|
||||
LaunchAttachCommonConfig: defaultLaunchAttachCommonConfig,
|
||||
}
|
||||
defaultAttachConfig = AttachConfig{
|
||||
Mode: "local",
|
||||
LaunchAttachCommonConfig: defaultLaunchAttachCommonConfig,
|
||||
}
|
||||
)
|
||||
|
||||
// LaunchConfig is the collection of launch request attributes recognized by DAP implementation.
|
||||
type LaunchConfig struct {
|
||||
// Acceptable values are:
|
||||
// "debug": compiles your program with optimizations disabled, starts and attaches to it.
|
||||
// "test": compiles your unit test program with optimizations disabled, starts and attaches to it.
|
||||
// "exec": executes a precompiled binary and begins a debug session.
|
||||
// "replay": replays an rr trace.
|
||||
// "core": examines a core dump.
|
||||
//
|
||||
// Default is "debug".
|
||||
Mode string `json:"mode,omitempty"`
|
||||
|
||||
// Path to the program folder (or any go file within that folder)
|
||||
// when in `debug` or `test` mode, and to the pre-built binary file
|
||||
// to debug in `exec` mode.
|
||||
// If it is not an absolute path, it will be interpreted as a path
|
||||
// relative to Delve's working directory.
|
||||
// Required when mode is `debug`, `test`, `exec`, and `core`.
|
||||
Program string `json:"program,omitempty"`
|
||||
|
||||
// Command line arguments passed to the debugged program.
|
||||
// Relative paths used in Args will be interpreted as paths relative
|
||||
// to `cwd`.
|
||||
Args []string `json:"args,omitempty"`
|
||||
|
||||
// Working directory of the program being debugged.
|
||||
// If a relative path is provided, it will be interpreted as
|
||||
// a relative path to Delve's working directory. This is
|
||||
// similar to `dlv --wd` flag.
|
||||
//
|
||||
// If not specified or empty, Delve's working directory is
|
||||
// used by default. But for `test` mode, Delve tries to find
|
||||
// the test's package source directory and run tests from there.
|
||||
// This matches the behavior of `dlv test` and `go test`.
|
||||
Cwd string `json:"cwd,omitempty"`
|
||||
|
||||
// Build flags, to be passed to the Go compiler.
|
||||
// Relative paths used in BuildFlags will be interpreted as paths
|
||||
// relative to Delve's current working directory.
|
||||
//
|
||||
// It should be a string like `dlv --build-flags`, or
|
||||
// an array of strings that is augmented when invoking the go build or
|
||||
// test command through os/exec.Command API.
|
||||
// For example, both forms are acceptable.
|
||||
// "buildFlags": "-tags=integration -ldflags='-X main.Hello=World'"
|
||||
// or
|
||||
// "buildFlags": ["-tags=integration", "-ldflags=-X main.Hello=World"]
|
||||
// Using other types is an error.
|
||||
BuildFlags BuildFlags `json:"buildFlags,omitempty"`
|
||||
|
||||
// Output path for the binary of the debuggee.
|
||||
// Relative path is interpreted as the path relative to
|
||||
// the Delve's current working directory.
|
||||
// This is deleted after the debug session ends.
|
||||
Output string `json:"output,omitempty"`
|
||||
|
||||
// NoDebug is used to run the program without debugging.
|
||||
NoDebug bool `json:"noDebug,omitempty"`
|
||||
|
||||
// TraceDirPath is the trace directory path for replay mode.
|
||||
// Relative path is interpreted as a path relative to Delve's
|
||||
// current working directory.
|
||||
// This is required for "replay" mode but unused in other modes.
|
||||
TraceDirPath string `json:"traceDirPath,omitempty"`
|
||||
|
||||
// CoreFilePath is the core file path for core mode.
|
||||
//
|
||||
// This is required for "core" mode but unused in other modes.
|
||||
CoreFilePath string `json:"coreFilePath,omitempty"`
|
||||
|
||||
// DlvCwd is the new working directory for Delve server.
|
||||
// If specified, the server will change its working
|
||||
// directory to the specified directory using os.Chdir.
|
||||
// Any other launch attributes with relative paths interpreted
|
||||
// using Delve's working directory will use this new directory.
|
||||
// When Delve needs to build the program (in debug/test modes),
|
||||
// it will run the go command from this directory as well.
|
||||
//
|
||||
// If a relative path is provided as DlvCwd, it will be
|
||||
// interpreted as a path relative to Delve's current working
|
||||
// directory.
|
||||
DlvCwd string `json:"dlvCwd,omitempty"`
|
||||
|
||||
// Env specifies optional environment variables for Delve server
|
||||
// in addition to the environment variables Delve initially
|
||||
// started with.
|
||||
// Variables with 'nil' values can be used to unset the named
|
||||
// environment variables.
|
||||
// Values are interpreted verbatim. Variable substitution or
|
||||
// reference to other environment variables is not supported.
|
||||
Env map[string]*string `json:"env,omitempty"`
|
||||
|
||||
// The output mode specifies how to handle the program's output.
|
||||
OutputMode string `json:"outputMode,omitempty"`
|
||||
|
||||
LaunchAttachCommonConfig
|
||||
}
|
||||
|
||||
// LaunchAttachCommonConfig is the attributes common in both launch/attach requests.
|
||||
type LaunchAttachCommonConfig struct {
|
||||
// Automatically stop program after launch or attach.
|
||||
StopOnEntry bool `json:"stopOnEntry,omitempty"`
|
||||
|
||||
// Backend used for debugging. See `dlv backend` for allowed values.
|
||||
// Default is "default".
|
||||
Backend string `json:"backend,omitempty"`
|
||||
|
||||
// Maximum depth of stack trace to return.
|
||||
// Default is 50.
|
||||
StackTraceDepth int `json:"stackTraceDepth,omitempty"`
|
||||
|
||||
// Boolean value to indicate whether global package variables
|
||||
// should be shown in the variables pane or not.
|
||||
ShowGlobalVariables bool `json:"showGlobalVariables,omitempty"`
|
||||
|
||||
// Boolean value to indicate whether registers should be shown
|
||||
// in the variables pane or not.
|
||||
ShowRegisters bool `json:"showRegisters,omitempty"`
|
||||
|
||||
// Boolean value to indicate whether system goroutines
|
||||
// should be hidden from the call stack view.
|
||||
HideSystemGoroutines bool `json:"hideSystemGoroutines,omitempty"`
|
||||
|
||||
// String value to indicate which system goroutines should be
|
||||
// shown in the call stack view. See filtering documentation:
|
||||
// https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md#goroutines
|
||||
GoroutineFilters string `json:"goroutineFilters,omitempty"`
|
||||
|
||||
// Array of string values indicating the keys of pprof labels to show as a
|
||||
// goroutine name in the threads view. If the array has one element, only
|
||||
// that label's value will be shown; otherwise, each of the labels will be
|
||||
// shown as "key:value". To show all labels, specify the single element "*".
|
||||
ShowPprofLabels []string `json:"showPprofLabels,omitempty"`
|
||||
|
||||
// An array of mappings from a local path (client) to the remote path (debugger).
|
||||
// This setting is useful when working in a file system with symbolic links,
|
||||
// running remote debugging, or debugging an executable compiled externally.
|
||||
// The debug adapter will replace the local path with the remote path in all of the calls.
|
||||
// See also Documentation/cli/substitutepath.md.
|
||||
SubstitutePath []SubstitutePath `json:"substitutePath,omitempty"`
|
||||
}
|
||||
|
||||
// SubstitutePath defines a mapping from a local path to the remote path.
|
||||
// Both 'from' and 'to' must be specified and non-null.
|
||||
// Empty values can be used to add or remove absolute path prefixes when mapping.
|
||||
// For example, mapping with empty 'to' can be used to work with binaries with trimmed paths.
|
||||
type SubstitutePath struct {
|
||||
// The local path to be replaced when passing paths to the debugger.
|
||||
From string `json:"from,omitempty"`
|
||||
// The remote path to be replaced when passing paths back to the client.
|
||||
To string `json:"to,omitempty"`
|
||||
}
|
||||
|
||||
func (m *SubstitutePath) UnmarshalJSON(data []byte) error {
|
||||
// use custom unmarshal to check if both from/to are set.
|
||||
type tmpType struct {
|
||||
From *string
|
||||
To *string
|
||||
}
|
||||
var tmp tmpType
|
||||
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
if _, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
return fmt.Errorf(`cannot use %s as 'substitutePath' of type {"from":string, "to":string}`, data)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if tmp.From == nil || tmp.To == nil {
|
||||
return errors.New("'substitutePath' requires both 'from' and 'to' entries")
|
||||
}
|
||||
*m = SubstitutePath{*tmp.From, *tmp.To}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachConfig is the collection of attach request attributes recognized by DAP implementation.
|
||||
// 'processId' and 'waitFor' are mutually exclusive, and can't be specified at the same time.
|
||||
type AttachConfig struct {
|
||||
// Acceptable values are:
|
||||
// "local": attaches to the local process with the given ProcessID.
|
||||
// "remote": expects the debugger to already be running to "attach" to an in-progress debug session.
|
||||
//
|
||||
// Default is "local".
|
||||
Mode string `json:"mode"`
|
||||
|
||||
// The numeric ID of the process to be debugged.
|
||||
ProcessID int `json:"processId,omitempty"`
|
||||
|
||||
// Wait for a process with a name beginning with this prefix.
|
||||
AttachWaitFor string `json:"waitFor,omitempty"`
|
||||
|
||||
LaunchAttachCommonConfig
|
||||
}
|
||||
|
||||
// unmarshalLaunchAttachArgs wraps unmarshaling of launch/attach request's
|
||||
// arguments attribute. Upon unmarshal failure, it returns an error massaged
|
||||
// to be suitable for end-users.
|
||||
func unmarshalLaunchAttachArgs(input json.RawMessage, config interface{}) error {
|
||||
if err := json.Unmarshal(input, config); err != nil {
|
||||
if uerr, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
// Format json.UnmarshalTypeError error string in our own way. E.g.,
|
||||
// "json: cannot unmarshal number into Go struct field LaunchArgs.substitutePath of type dap.SubstitutePath"
|
||||
// => "cannot unmarshal number into 'substitutePath' of type {from:string, to:string}"
|
||||
// "json: cannot unmarshal number into Go struct field LaunchArgs.program of type string" (go1.16)
|
||||
// => "cannot unmarshal number into 'program' of type string"
|
||||
typ := uerr.Type.String()
|
||||
if uerr.Field == "substitutePath" {
|
||||
typ = `{"from":string, "to":string}`
|
||||
}
|
||||
return fmt.Errorf("cannot unmarshal %v into %q of type %v", uerr.Value, uerr.Field, typ)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prettyPrint(config interface{}) string {
|
||||
pretty, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%#v", config)
|
||||
}
|
||||
return string(pretty)
|
||||
}
|
||||
|
||||
// BuildFlags is either string or []string.
|
||||
type BuildFlags struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (s *BuildFlags) UnmarshalJSON(b []byte) error {
|
||||
if v := string(b); v == "" || v == "null" {
|
||||
s.value = nil
|
||||
return nil
|
||||
}
|
||||
var strs []string
|
||||
if err := json.Unmarshal(b, &strs); err == nil {
|
||||
s.value = strs
|
||||
return nil
|
||||
}
|
||||
var str string
|
||||
if err := json.Unmarshal(b, &str); err != nil {
|
||||
s.value = nil
|
||||
if uerr, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
return fmt.Errorf(`cannot unmarshal %v into "buildFlags" of type []string or string`, uerr.Value)
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.value = str
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
package debugger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
attachErrorMessage = attachErrorMessageLinux
|
||||
}
|
||||
|
||||
//lint:file-ignore ST1005 errors here can be capitalized
|
||||
|
||||
func attachErrorMessageLinux(pid int, err error) error {
|
||||
fallbackerr := fmt.Errorf("could not attach to pid %d: %s", pid, err)
|
||||
if serr, ok := err.(syscall.Errno); ok {
|
||||
switch serr {
|
||||
case syscall.EPERM:
|
||||
bs, err := os.ReadFile("/proc/sys/kernel/yama/ptrace_scope")
|
||||
if err == nil && len(bs) >= 1 && bs[0] != '0' {
|
||||
// Yama documentation: https://www.kernel.org/doc/Documentation/security/Yama.txt
|
||||
return fmt.Errorf("Could not attach to pid %d: this could be caused by a kernel security setting, try writing \"0\" to /proc/sys/kernel/yama/ptrace_scope", pid)
|
||||
}
|
||||
fi, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
|
||||
if err != nil {
|
||||
return fallbackerr
|
||||
}
|
||||
if fi.Sys().(*syscall.Stat_t).Uid != uint32(os.Getuid()) {
|
||||
return fmt.Errorf("Could not attach to pid %d: current user does not own the process", pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallbackerr
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package debugger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-delve/delve/pkg/gobuild"
|
||||
protest "github.com/go-delve/delve/pkg/proc/test"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
)
|
||||
|
||||
func TestDebugger_LaunchNoMain(t *testing.T) {
|
||||
fixturesDir := protest.FindFixturesDir()
|
||||
nomaindir := filepath.Join(fixturesDir, "nomaindir")
|
||||
debugname := "debug"
|
||||
exepath := filepath.Join(nomaindir, debugname)
|
||||
defer os.Remove(exepath)
|
||||
if err := gobuild.GoBuild(debugname, []string{nomaindir}, fmt.Sprintf("-o %s", exepath)); err != nil {
|
||||
t.Fatalf("go build error %v", err)
|
||||
}
|
||||
|
||||
d := new(Debugger)
|
||||
_, err := d.Launch([]string{exepath}, ".")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but none was generated")
|
||||
}
|
||||
if err != api.ErrNotExecutable {
|
||||
t.Fatalf("expected error \"%v\" got \"%v\"", api.ErrNotExecutable, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugger_LaunchInvalidFormat(t *testing.T) {
|
||||
fixturesDir := protest.FindFixturesDir()
|
||||
buildtestdir := filepath.Join(fixturesDir, "buildtest")
|
||||
debugname := "debug"
|
||||
switchOS := map[string]string{
|
||||
"darwin": "linux",
|
||||
"windows": "linux",
|
||||
"freebsd": "windows",
|
||||
"linux": "windows",
|
||||
}
|
||||
if runtime.GOARCH == "arm64" && runtime.GOOS == "linux" {
|
||||
t.Setenv("GOARCH", "amd64")
|
||||
}
|
||||
if runtime.GOARCH == "ppc64le" && runtime.GOOS == "linux" {
|
||||
t.Setenv("GOARCH", "amd64")
|
||||
}
|
||||
t.Setenv("GOOS", switchOS[runtime.GOOS])
|
||||
exepath := filepath.Join(buildtestdir, debugname)
|
||||
if err := gobuild.GoBuild(debugname, []string{buildtestdir}, fmt.Sprintf("-o %s", exepath)); err != nil {
|
||||
t.Fatalf("go build error %v", err)
|
||||
}
|
||||
defer os.Remove(exepath)
|
||||
|
||||
d := new(Debugger)
|
||||
_, err := d.Launch([]string{exepath}, ".")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but none was generated")
|
||||
}
|
||||
if err != api.ErrNotExecutable {
|
||||
t.Fatalf("expected error %q got \"%v\"", api.ErrNotExecutable, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugger_LaunchCurrentDir(t *testing.T) {
|
||||
fixturesDir := protest.FindFixturesDir()
|
||||
testDir := filepath.Join(fixturesDir, "buildtest")
|
||||
debugname := "debug"
|
||||
exepath := filepath.Join(testDir, debugname)
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chdir(originalPath)
|
||||
defer func() {
|
||||
if err := os.Remove(exepath); err != nil {
|
||||
t.Fatalf("error removing executable %v", err)
|
||||
}
|
||||
}()
|
||||
if err := gobuild.GoBuild(debugname, []string{testDir}, fmt.Sprintf("-o %s", exepath)); err != nil {
|
||||
t.Fatalf("go build error %v", err)
|
||||
}
|
||||
|
||||
os.Chdir(testDir)
|
||||
|
||||
d := new(Debugger)
|
||||
d.config = &Config{}
|
||||
_, err = d.Launch([]string{debugname}, ".")
|
||||
if err == nil {
|
||||
t.Fatal("expected error but none was generated")
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "unknown backend") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//go:build !windows
|
||||
|
||||
package debugger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/go-delve/delve/pkg/gobuild"
|
||||
protest "github.com/go-delve/delve/pkg/proc/test"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
)
|
||||
|
||||
func TestDebugger_LaunchNoExecutablePerm(t *testing.T) {
|
||||
fixturesDir := protest.FindFixturesDir()
|
||||
buildtestdir := filepath.Join(fixturesDir, "buildtest")
|
||||
debugname := "debug"
|
||||
switchOS := map[string]string{
|
||||
"darwin": "linux",
|
||||
"windows": "linux",
|
||||
"freebsd": "windows",
|
||||
"linux": "windows",
|
||||
}
|
||||
if runtime.GOARCH == "arm64" && runtime.GOOS == "linux" {
|
||||
t.Setenv("GOARCH", "amd64")
|
||||
}
|
||||
if runtime.GOARCH == "ppc64le" && runtime.GOOS == "linux" {
|
||||
t.Setenv("GOARCH", "amd64")
|
||||
}
|
||||
t.Setenv("GOOS", switchOS[runtime.GOOS])
|
||||
exepath := filepath.Join(buildtestdir, debugname)
|
||||
defer os.Remove(exepath)
|
||||
if err := gobuild.GoBuild(debugname, []string{buildtestdir}, fmt.Sprintf("-o %s", exepath)); err != nil {
|
||||
t.Fatalf("go build error %v", err)
|
||||
}
|
||||
if err := os.Chmod(exepath, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := new(Debugger)
|
||||
_, err := d.Launch([]string{exepath}, ".")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but none was generated")
|
||||
}
|
||||
if err != api.ErrNotExecutable {
|
||||
t.Fatalf("expected error %q got \"%v\"", api.ErrNotExecutable, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugger_LaunchWithTTY(t *testing.T) {
|
||||
if os.Getenv("CI") == "true" {
|
||||
if _, err := exec.LookPath("lsof"); err != nil {
|
||||
t.Skip("skipping test in CI, system does not contain lsof")
|
||||
}
|
||||
}
|
||||
// Ensure no env meddling is leftover from previous tests.
|
||||
t.Setenv("GOOS", runtime.GOOS)
|
||||
t.Setenv("GOARCH", runtime.GOARCH)
|
||||
|
||||
p, tty, err := pty.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.Close()
|
||||
defer tty.Close()
|
||||
|
||||
fixturesDir := protest.FindFixturesDir()
|
||||
buildtestdir := filepath.Join(fixturesDir, "buildtest")
|
||||
debugname := "debugtty"
|
||||
exepath := filepath.Join(buildtestdir, debugname)
|
||||
if err := gobuild.GoBuild(debugname, []string{buildtestdir}, fmt.Sprintf("-o %s", exepath)); err != nil {
|
||||
t.Fatalf("go build error %v", err)
|
||||
}
|
||||
defer os.Remove(exepath)
|
||||
var backend string
|
||||
protest.DefaultTestBackend(&backend)
|
||||
conf := &Config{TTY: tty.Name(), Backend: backend}
|
||||
pArgs := []string{exepath}
|
||||
d, err := New(conf, pArgs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
openFileCmd, wantTTYName := "lsof", tty.Name()
|
||||
if runtime.GOOS == "freebsd" {
|
||||
openFileCmd = "fstat"
|
||||
wantTTYName = strings.TrimPrefix(wantTTYName, "/dev/")
|
||||
}
|
||||
cmd := exec.Command(openFileCmd, "-p", fmt.Sprintf("%d", d.ProcessPid()))
|
||||
result, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(result, []byte(wantTTYName)) {
|
||||
t.Fatalf("process open file list does not contain expected tty %q", wantTTYName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package sameuser provides utilities for checking users of a local connection.
|
||||
// Only works in Linux.
|
||||
package sameuser
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package sameuser
|
||||
|
||||
import "net"
|
||||
|
||||
func CanAccept(_, _, _ net.Addr) bool {
|
||||
return true
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package sameuser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-delve/delve/pkg/logflags"
|
||||
)
|
||||
|
||||
// for testing
|
||||
var (
|
||||
uid = os.Getuid()
|
||||
readFile = os.ReadFile
|
||||
)
|
||||
|
||||
type errConnectionNotFound struct {
|
||||
filename string
|
||||
}
|
||||
|
||||
func (e *errConnectionNotFound) Error() string {
|
||||
return fmt.Sprintf("connection not found in %s", e.filename)
|
||||
}
|
||||
|
||||
func sameUserForHexLocalAddr(filename, localAddr, remoteAddr string) (bool, error) {
|
||||
b, err := readFile(filename)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
|
||||
// The format contains whitespace padding (%4d, %5u), so we use
|
||||
// fmt.Sscanf instead of splitting on whitespace.
|
||||
var (
|
||||
sl int
|
||||
readLocalAddr, readRemoteAddr string
|
||||
state int
|
||||
queue, timer string
|
||||
retransmit int
|
||||
remoteUID uint
|
||||
)
|
||||
// Note that we must use %d where the kernel format uses %4d or %5u:
|
||||
// - %4d fails to parse for large number of entries (len(sl) > 4)
|
||||
// - %u is not understood by the fmt package (%U is something else)
|
||||
// - %5d cuts off longer uids (e.g. 149098 on gLinux)
|
||||
n, err := fmt.Sscanf(line, "%d: %s %s %02X %s %s %08X %d",
|
||||
&sl, &readLocalAddr, &readRemoteAddr, &state, &queue, &timer, &retransmit, &remoteUID)
|
||||
if n != 8 || err != nil {
|
||||
continue // invalid line (e.g. header line)
|
||||
}
|
||||
if readLocalAddr != remoteAddr || readRemoteAddr != localAddr {
|
||||
// this check is deliberately crossed, the (readLocalAddr,
|
||||
// readRemoteAddr) pair is from the point of view of the client, the
|
||||
// (localAddr, remoteAddr) is from the point of view of the server.
|
||||
continue
|
||||
}
|
||||
same := uid == int(remoteUID)
|
||||
if !same && logflags.Any() {
|
||||
log.Printf("connection from different user (remote: %d, local: %d) detected: %v", remoteUID, uid, line)
|
||||
}
|
||||
return same, nil
|
||||
}
|
||||
return false, &errConnectionNotFound{filename}
|
||||
}
|
||||
|
||||
func addrToHex4(addr *net.TCPAddr) string {
|
||||
// For details about the format, see the kernel side implementation:
|
||||
// https://elixir.bootlin.com/linux/v5.2.2/source/net/ipv4/tcp_ipv4.c#L2375
|
||||
b := addr.IP.To4()
|
||||
return fmt.Sprintf("%02X%02X%02X%02X:%04X", b[3], b[2], b[1], b[0], addr.Port)
|
||||
}
|
||||
|
||||
func addrToHex6(addr *net.TCPAddr) string {
|
||||
a16 := addr.IP.To16()
|
||||
// For details about the format, see the kernel side implementation:
|
||||
// https://elixir.bootlin.com/linux/v5.2.2/source/net/ipv6/tcp_ipv6.c#L1792
|
||||
words := make([]uint32, 4)
|
||||
if err := binary.Read(bytes.NewReader(a16), binary.LittleEndian, words); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("%08X%08X%08X%08X:%04X", words[0], words[1], words[2], words[3], addr.Port)
|
||||
}
|
||||
|
||||
func sameUserForRemoteAddr4(localAddr, remoteAddr *net.TCPAddr) (bool, error) {
|
||||
r, err := sameUserForHexLocalAddr("/proc/net/tcp", addrToHex4(localAddr), addrToHex4(remoteAddr))
|
||||
if _, isNotFound := err.(*errConnectionNotFound); isNotFound {
|
||||
// See Issue #1835
|
||||
r, err2 := sameUserForHexLocalAddr("/proc/net/tcp6", "0000000000000000FFFF0000"+addrToHex4(localAddr), "0000000000000000FFFF0000"+addrToHex4(remoteAddr))
|
||||
if err2 == nil {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func sameUserForRemoteAddr6(localAddr, remoteAddr *net.TCPAddr) (bool, error) {
|
||||
return sameUserForHexLocalAddr("/proc/net/tcp6", addrToHex6(localAddr), addrToHex6(remoteAddr))
|
||||
}
|
||||
|
||||
func sameUserForRemoteAddr(localAddr, remoteAddr *net.TCPAddr) (bool, error) {
|
||||
if remoteAddr.IP.To4() == nil {
|
||||
return sameUserForRemoteAddr6(localAddr, remoteAddr)
|
||||
}
|
||||
return sameUserForRemoteAddr4(localAddr, remoteAddr)
|
||||
}
|
||||
|
||||
func CanAccept(listenAddr, localAddr, remoteAddr net.Addr) bool {
|
||||
laddr, ok := listenAddr.(*net.TCPAddr)
|
||||
if !ok || !laddr.IP.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
remoteAddrTCP := remoteAddr.(*net.TCPAddr)
|
||||
localAddrTCP := localAddr.(*net.TCPAddr)
|
||||
|
||||
same, err := sameUserForRemoteAddr(localAddrTCP, remoteAddrTCP)
|
||||
if err != nil {
|
||||
log.Printf("cannot check remote address: %v", err)
|
||||
}
|
||||
if !same {
|
||||
if logflags.Any() {
|
||||
log.Printf("closing connection from different user (%v): connections to localhost are only accepted from the same UNIX user for security reasons", remoteAddrTCP)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "closing connection from different user (%v): connections to localhost are only accepted from the same UNIX user for security reasons\n", remoteAddrTCP)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package sameuser
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSameUserForRemoteAddr(t *testing.T) {
|
||||
uid = 149098
|
||||
var proc string
|
||||
readFile = func(string) ([]byte, error) {
|
||||
return []byte(proc), nil
|
||||
}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
proc string
|
||||
localAddr, remoteAddr *net.TCPAddr
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "ipv4-same",
|
||||
proc: ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
21: 0100007F:E682 0100007F:0FC8 01 00000000:00000000 00:00000000 00000000 149098 0 8420541 2 0000000000000000 20 0 0 10 -1 `,
|
||||
localAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4040},
|
||||
remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 59010},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "ipv4-not-found",
|
||||
proc: ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
21: 0100007F:E682 0100007F:0FC8 01 00000000:00000000 00:00000000 00000000 149098 0 8420541 2 0000000000000000 20 0 0 10 -1 `,
|
||||
localAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4040},
|
||||
remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 2342},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "ipv4-different-uid",
|
||||
proc: ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
21: 0100007F:E682 0100007F:0FC8 01 00000000:00000000 00:00000000 00000000 149097 0 8420541 2 0000000000000000 20 0 0 10 -1 `,
|
||||
localAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4040},
|
||||
remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 59010},
|
||||
want: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "ipv6-same",
|
||||
proc: ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
5: 00000000000000000000000001000000:D3E4 00000000000000000000000001000000:0FC8 01 00000000:00000000 00:00000000 00000000 149098 0 8425526 2 0000000000000000 20 0 0 10 -1
|
||||
6: 00000000000000000000000001000000:0FC8 00000000000000000000000001000000:D3E4 01 00000000:00000000 00:00000000 00000000 149098 0 8424744 1 0000000000000000 20 0 0 10 -1`,
|
||||
localAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 4040},
|
||||
remoteAddr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 54244},
|
||||
want: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
proc = tt.proc
|
||||
// The returned error is for reporting only.
|
||||
same, _ := sameUserForRemoteAddr(tt.localAddr, tt.remoteAddr)
|
||||
if got, want := same, tt.want; got != want {
|
||||
t.Errorf("sameUserForRemoteAddr(%v, %v) = %v, want %v", tt.localAddr, tt.remoteAddr, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ListenerPipe returns a full-duplex in-memory connection, like net.Pipe.
|
||||
// Unlike net.Pipe one end of the connection is returned as an object
|
||||
// satisfying the net.Listener interface.
|
||||
// The first call to the Accept method of this object will return a net.Conn
|
||||
// connected to the other net.Conn returned by ListenerPipe.
|
||||
// Any subsequent calls to Accept will block until the listener is closed.
|
||||
func ListenerPipe() (net.Listener, net.Conn) {
|
||||
conn0, conn1 := net.Pipe()
|
||||
return &preconnectedListener{conn: conn0, closech: make(chan struct{})}, conn1
|
||||
}
|
||||
|
||||
// preconnectedListener satisfies the net.Listener interface by accepting a
|
||||
// single pre-established connection.
|
||||
// The first call to Accept will return the conn field, any subsequent call
|
||||
// will block until the listener is closed.
|
||||
type preconnectedListener struct {
|
||||
accepted bool
|
||||
conn net.Conn
|
||||
closech chan struct{}
|
||||
closeMu sync.Mutex
|
||||
acceptMu sync.Mutex
|
||||
}
|
||||
|
||||
// Accept returns the pre-established connection the first time it's called,
|
||||
// it blocks until the listener is closed on every subsequent call.
|
||||
func (l *preconnectedListener) Accept() (net.Conn, error) {
|
||||
l.acceptMu.Lock()
|
||||
defer l.acceptMu.Unlock()
|
||||
if !l.accepted {
|
||||
l.accepted = true
|
||||
return l.conn, nil
|
||||
}
|
||||
<-l.closech
|
||||
return nil, errors.New("accept failed: listener closed")
|
||||
}
|
||||
|
||||
// Close closes the listener.
|
||||
func (l *preconnectedListener) Close() error {
|
||||
l.closeMu.Lock()
|
||||
defer l.closeMu.Unlock()
|
||||
if l.closech == nil {
|
||||
return nil
|
||||
}
|
||||
close(l.closech)
|
||||
l.closech = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the listener's network address.
|
||||
func (l *preconnectedListener) Addr() net.Addr {
|
||||
return l.conn.LocalAddr()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package rpc1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/go-delve/delve/service/api"
|
||||
)
|
||||
|
||||
// RPCClient is a RPC service.Client.
|
||||
type RPCClient struct {
|
||||
addr string
|
||||
client *rpc.Client
|
||||
haltMu sync.Mutex
|
||||
haltReq bool
|
||||
}
|
||||
|
||||
var errAPIUnsupported = errors.New("unsupported")
|
||||
|
||||
// NewClient creates a new RPCClient.
|
||||
func NewClient(addr string) *RPCClient {
|
||||
client, err := jsonrpc.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
return &RPCClient{
|
||||
addr: addr,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RPCClient) ProcessPid() int {
|
||||
var pid int
|
||||
c.call("ProcessPid", nil, &pid)
|
||||
return pid
|
||||
}
|
||||
|
||||
func (c *RPCClient) Detach(kill bool) error {
|
||||
return c.call("Detach", kill, nil)
|
||||
}
|
||||
|
||||
func (c *RPCClient) Restart() error {
|
||||
return c.call("Restart", nil, nil)
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetState() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("State", nil, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Continue() <-chan *api.DebuggerState {
|
||||
ch := make(chan *api.DebuggerState)
|
||||
c.haltMu.Lock()
|
||||
c.haltReq = false
|
||||
c.haltMu.Unlock()
|
||||
go func() {
|
||||
for {
|
||||
c.haltMu.Lock()
|
||||
if c.haltReq {
|
||||
c.haltMu.Unlock()
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
c.haltMu.Unlock()
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.Continue}, state)
|
||||
if err != nil {
|
||||
state.Err = err
|
||||
}
|
||||
if state.Exited {
|
||||
// Error types apparently cannot be marshalled by Go correctly. Must reset error here.
|
||||
//lint:ignore ST1005 backwards compatibility
|
||||
state.Err = fmt.Errorf("Process %d has exited with status %d", c.ProcessPid(), state.ExitStatus)
|
||||
}
|
||||
ch <- state
|
||||
if err != nil || state.Exited {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
isbreakpoint := false
|
||||
istracepoint := true
|
||||
for i := range state.Threads {
|
||||
if state.Threads[i].Breakpoint != nil {
|
||||
isbreakpoint = true
|
||||
istracepoint = istracepoint && state.Threads[i].Breakpoint.Tracepoint
|
||||
}
|
||||
}
|
||||
|
||||
if !isbreakpoint || !istracepoint {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *RPCClient) Next() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.Next}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Step() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.Step}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Call(expr string) (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.Call, Expr: expr}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) StepInstruction() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.StepInstruction}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ReverseStepInstruction() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.ReverseStepInstruction}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SwitchThread(threadID int) (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
cmd := &api.DebuggerCommand{
|
||||
Name: api.SwitchThread,
|
||||
ThreadID: threadID,
|
||||
}
|
||||
err := c.call("Command", cmd, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SwitchGoroutine(goroutineID int) (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
cmd := &api.DebuggerCommand{
|
||||
Name: api.SwitchGoroutine,
|
||||
GoroutineID: int64(goroutineID),
|
||||
}
|
||||
err := c.call("Command", cmd, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Halt() (*api.DebuggerState, error) {
|
||||
state := new(api.DebuggerState)
|
||||
c.haltMu.Lock()
|
||||
c.haltReq = true
|
||||
c.haltMu.Unlock()
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: api.Halt}, state)
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetBreakpoint(id int) (*api.Breakpoint, error) {
|
||||
breakpoint := new(api.Breakpoint)
|
||||
err := c.call("GetBreakpoint", id, breakpoint)
|
||||
return breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetBreakpointByName(name string) (*api.Breakpoint, error) {
|
||||
breakpoint := new(api.Breakpoint)
|
||||
err := c.call("GetBreakpointByName", name, breakpoint)
|
||||
return breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) {
|
||||
newBreakpoint := new(api.Breakpoint)
|
||||
err := c.call("CreateBreakpoint", breakPoint, &newBreakpoint)
|
||||
return newBreakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListBreakpoints() ([]*api.Breakpoint, error) {
|
||||
var breakpoints []*api.Breakpoint
|
||||
err := c.call("ListBreakpoints", nil, &breakpoints)
|
||||
return breakpoints, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ClearBreakpoint(id int) (*api.Breakpoint, error) {
|
||||
bp := new(api.Breakpoint)
|
||||
err := c.call("ClearBreakpoint", id, bp)
|
||||
return bp, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ClearBreakpointByName(name string) (*api.Breakpoint, error) {
|
||||
bp := new(api.Breakpoint)
|
||||
err := c.call("ClearBreakpointByName", name, bp)
|
||||
return bp, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
|
||||
err := c.call("AmendBreakpoint", bp, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *RPCClient) CancelNext() error {
|
||||
return errAPIUnsupported
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
|
||||
var threads []*api.Thread
|
||||
err := c.call("ListThreads", nil, &threads)
|
||||
return threads, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetThread(id int) (*api.Thread, error) {
|
||||
thread := new(api.Thread)
|
||||
err := c.call("GetThread", id, &thread)
|
||||
return thread, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) EvalVariable(scope api.EvalScope, symbol string) (*api.Variable, error) {
|
||||
v := new(api.Variable)
|
||||
err := c.call("EvalSymbol", EvalSymbolArgs{scope, symbol}, v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SetVariable(scope api.EvalScope, symbol, value string) error {
|
||||
var unused int
|
||||
return c.call("SetSymbol", SetSymbolArgs{scope, symbol, value}, &unused)
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListSources(filter string) ([]string, error) {
|
||||
var sources []string
|
||||
err := c.call("ListSources", filter, &sources)
|
||||
return sources, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListFunctions(filter string) ([]string, error) {
|
||||
var funcs []string
|
||||
err := c.call("ListFunctions", filter, &funcs)
|
||||
return funcs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListTypes(filter string) ([]string, error) {
|
||||
var types []string
|
||||
err := c.call("ListTypes", filter, &types)
|
||||
return types, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListPackageVariables(filter string) ([]api.Variable, error) {
|
||||
var vars []api.Variable
|
||||
err := c.call("ListPackageVars", filter, &vars)
|
||||
return vars, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListPackageVariablesFor(threadID int, filter string) ([]api.Variable, error) {
|
||||
var vars []api.Variable
|
||||
err := c.call("ListThreadPackageVars", &ThreadListArgs{Id: threadID, Filter: filter}, &vars)
|
||||
return vars, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListLocalVariables(scope api.EvalScope) ([]api.Variable, error) {
|
||||
var vars []api.Variable
|
||||
err := c.call("ListLocalVars", scope, &vars)
|
||||
return vars, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListRegisters() (string, error) {
|
||||
var regs string
|
||||
err := c.call("ListRegisters", nil, ®s)
|
||||
return regs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListFunctionArgs(scope api.EvalScope) ([]api.Variable, error) {
|
||||
var vars []api.Variable
|
||||
err := c.call("ListFunctionArgs", scope, &vars)
|
||||
return vars, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListGoroutines() ([]*api.Goroutine, error) {
|
||||
var goroutines []*api.Goroutine
|
||||
err := c.call("ListGoroutines", nil, &goroutines)
|
||||
return goroutines, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Stacktrace(goroutineId, depth int, full bool) ([]api.Stackframe, error) {
|
||||
var locations []api.Stackframe
|
||||
err := c.call("StacktraceGoroutine", &StacktraceGoroutineArgs{Id: goroutineId, Depth: depth, Full: full}, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) AttachedToExistingProcess() bool {
|
||||
var answer bool
|
||||
c.call("AttachedToExistingProcess", nil, &answer)
|
||||
return answer
|
||||
}
|
||||
|
||||
func (c *RPCClient) FindLocation(scope api.EvalScope, loc string) ([]api.Location, error) {
|
||||
var answer []api.Location
|
||||
err := c.call("FindLocation", FindLocationArgs{scope, loc}, &answer)
|
||||
return answer, err
|
||||
}
|
||||
|
||||
// DisassembleRange disassembles code between startPC and endPC
|
||||
func (c *RPCClient) DisassembleRange(scope api.EvalScope, startPC, endPC uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
|
||||
var r api.AsmInstructions
|
||||
err := c.call("Disassemble", DisassembleRequest{scope, startPC, endPC, flavour}, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// DisassemblePC disassembles function containing pc
|
||||
func (c *RPCClient) DisassemblePC(scope api.EvalScope, pc uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
|
||||
var r api.AsmInstructions
|
||||
err := c.call("Disassemble", DisassembleRequest{scope, pc, 0, flavour}, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) call(method string, args, reply interface{}) error {
|
||||
return c.client.Call("RPCServer."+method, args, reply)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package rpc1 implements version 1 of Delve's API and is only
|
||||
// kept for backwards compatibility.
|
||||
// client.go is the old client code used by Delve's frontend (delve/cmd/dlv) and
|
||||
// is preserved for backwards compatibility with integration tests.
|
||||
package rpc1
|
||||
@@ -0,0 +1,330 @@
|
||||
package rpc1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-delve/delve/pkg/proc"
|
||||
"github.com/go-delve/delve/service"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
"github.com/go-delve/delve/service/debugger"
|
||||
)
|
||||
|
||||
var defaultLoadConfig = proc.LoadConfig{
|
||||
FollowPointers: true,
|
||||
MaxVariableRecurse: 1,
|
||||
MaxStringLen: 64,
|
||||
MaxArrayValues: 64,
|
||||
MaxStructFields: -1,
|
||||
}
|
||||
|
||||
type RPCServer struct {
|
||||
// config is all the information necessary to start the debugger and server.
|
||||
config *service.Config
|
||||
// debugger is a debugger service.
|
||||
debugger *debugger.Debugger
|
||||
}
|
||||
|
||||
func NewServer(config *service.Config, debugger *debugger.Debugger) *RPCServer {
|
||||
return &RPCServer{config, debugger}
|
||||
}
|
||||
|
||||
func (s *RPCServer) ProcessPid(arg1 interface{}, pid *int) error {
|
||||
*pid = s.debugger.ProcessPid()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) Detach(kill bool, ret *int) error {
|
||||
return s.debugger.Detach(kill)
|
||||
}
|
||||
|
||||
func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error {
|
||||
if s.config.Debugger.AttachPid != 0 {
|
||||
return errors.New("cannot restart process Delve did not create")
|
||||
}
|
||||
_, err := s.debugger.Restart(false, "", false, nil, [3]string{}, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *RPCServer) State(arg interface{}, state *api.DebuggerState) error {
|
||||
st, err := s.debugger.State(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*state = *st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) Command(command *api.DebuggerCommand, cb service.RPCCallback) {
|
||||
st, err := s.debugger.Command(command, cb.SetupDoneChan(), cb.DisconnectChan())
|
||||
cb.Return(st, err)
|
||||
}
|
||||
|
||||
func (s *RPCServer) GetBreakpoint(id int, breakpoint *api.Breakpoint) error {
|
||||
bp := s.debugger.FindBreakpoint(id)
|
||||
if bp == nil {
|
||||
return fmt.Errorf("no breakpoint with id %d", id)
|
||||
}
|
||||
*breakpoint = *bp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) GetBreakpointByName(name string, breakpoint *api.Breakpoint) error {
|
||||
bp := s.debugger.FindBreakpointByName(name)
|
||||
if bp == nil {
|
||||
return fmt.Errorf("no breakpoint with name %s", name)
|
||||
}
|
||||
*breakpoint = *bp
|
||||
return nil
|
||||
}
|
||||
|
||||
type StacktraceGoroutineArgs struct {
|
||||
Id int
|
||||
Depth int
|
||||
Full bool
|
||||
}
|
||||
|
||||
func (s *RPCServer) StacktraceGoroutine(args *StacktraceGoroutineArgs, locations *[]api.Stackframe) error {
|
||||
var loadcfg *proc.LoadConfig = nil
|
||||
if args.Full {
|
||||
loadcfg = &defaultLoadConfig
|
||||
}
|
||||
locs, err := s.debugger.Stacktrace(int64(args.Id), args.Depth, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*locations, err = s.debugger.ConvertStacktrace(locs, loadcfg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListBreakpoints(arg interface{}, breakpoints *[]*api.Breakpoint) error {
|
||||
*breakpoints = s.debugger.Breakpoints(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) CreateBreakpoint(bp, newBreakpoint *api.Breakpoint) error {
|
||||
if err := api.ValidBreakpointName(bp.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
createdbp, err := s.debugger.CreateBreakpoint(bp, "", nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*newBreakpoint = *createdbp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ClearBreakpoint(id int, breakpoint *api.Breakpoint) error {
|
||||
bp := s.debugger.FindBreakpoint(id)
|
||||
if bp == nil {
|
||||
return fmt.Errorf("no breakpoint with id %d", id)
|
||||
}
|
||||
deleted, err := s.debugger.ClearBreakpoint(bp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*breakpoint = *deleted
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ClearBreakpointByName(name string, breakpoint *api.Breakpoint) error {
|
||||
bp := s.debugger.FindBreakpointByName(name)
|
||||
if bp == nil {
|
||||
return fmt.Errorf("no breakpoint with name %s", name)
|
||||
}
|
||||
deleted, err := s.debugger.ClearBreakpoint(bp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*breakpoint = *deleted
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) AmendBreakpoint(amend *api.Breakpoint, unused *int) error {
|
||||
*unused = 0
|
||||
if err := api.ValidBreakpointName(amend.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.debugger.AmendBreakpoint(amend)
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListThreads(arg interface{}, threads *[]*api.Thread) (err error) {
|
||||
pthreads, err := s.debugger.Threads()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.debugger.LockTarget()
|
||||
defer s.debugger.UnlockTarget()
|
||||
*threads = api.ConvertThreads(pthreads, s.debugger.ConvertThreadBreakpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) GetThread(id int, thread *api.Thread) error {
|
||||
t, err := s.debugger.FindThread(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t == nil {
|
||||
return fmt.Errorf("no thread with id %d", id)
|
||||
}
|
||||
s.debugger.LockTarget()
|
||||
defer s.debugger.UnlockTarget()
|
||||
*thread = *api.ConvertThread(t, s.debugger.ConvertThreadBreakpoint(t))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListPackageVars(filter string, variables *[]api.Variable) error {
|
||||
vars, err := s.debugger.PackageVariables(filter, defaultLoadConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*variables = api.ConvertVars(vars)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ThreadListArgs struct {
|
||||
Id int
|
||||
Filter string
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListThreadPackageVars(args *ThreadListArgs, variables *[]api.Variable) error {
|
||||
vars, err := s.debugger.PackageVariables(args.Filter, defaultLoadConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*variables = api.ConvertVars(vars)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListRegisters(arg interface{}, registers *string) error {
|
||||
state, err := s.debugger.State(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dregs, err := s.debugger.ThreadRegisters(state.CurrentThread.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
regs := api.Registers(api.ConvertRegisters(dregs, s.debugger.DwarfRegisterToString, false))
|
||||
*registers = regs.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListLocalVars(scope api.EvalScope, variables *[]api.Variable) error {
|
||||
vars, err := s.debugger.LocalVariables(scope.GoroutineID, scope.Frame, scope.DeferredCall, defaultLoadConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*variables = api.ConvertVars(vars)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListFunctionArgs(scope api.EvalScope, variables *[]api.Variable) error {
|
||||
vars, err := s.debugger.FunctionArguments(scope.GoroutineID, scope.Frame, scope.DeferredCall, defaultLoadConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*variables = api.ConvertVars(vars)
|
||||
return nil
|
||||
}
|
||||
|
||||
type EvalSymbolArgs struct {
|
||||
Scope api.EvalScope
|
||||
Symbol string
|
||||
}
|
||||
|
||||
func (s *RPCServer) EvalSymbol(args EvalSymbolArgs, variable *api.Variable) error {
|
||||
v, err := s.debugger.EvalVariableInScope(args.Scope.GoroutineID, args.Scope.Frame, args.Scope.DeferredCall, args.Symbol, defaultLoadConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*variable = *api.ConvertVar(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetSymbolArgs struct {
|
||||
Scope api.EvalScope
|
||||
Symbol string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (s *RPCServer) SetSymbol(args SetSymbolArgs, unused *int) error {
|
||||
*unused = 0
|
||||
return s.debugger.SetVariableInScope(args.Scope.GoroutineID, args.Scope.Frame, args.Scope.DeferredCall, args.Symbol, args.Value)
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListSources(filter string, sources *[]string) error {
|
||||
ss, err := s.debugger.Sources(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*sources = ss
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListFunctions(filter string, followCalls int, funcs *[]string) error {
|
||||
fns, err := s.debugger.Functions(filter, followCalls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*funcs = fns
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListTypes(filter string, types *[]string) error {
|
||||
tps, err := s.debugger.Types(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*types = tps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) ListGoroutines(arg interface{}, goroutines *[]*api.Goroutine) error {
|
||||
gs, _, err := s.debugger.Goroutines(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.debugger.LockTarget()
|
||||
s.debugger.UnlockTarget()
|
||||
*goroutines = api.ConvertGoroutines(s.debugger.Target(), gs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RPCServer) AttachedToExistingProcess(arg interface{}, answer *bool) error {
|
||||
if s.config.Debugger.AttachPid != 0 {
|
||||
*answer = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindLocationArgs struct {
|
||||
Scope api.EvalScope
|
||||
Loc string
|
||||
}
|
||||
|
||||
func (s *RPCServer) FindLocation(args FindLocationArgs, answer *[]api.Location) error {
|
||||
var err error
|
||||
*answer, _, err = s.debugger.FindLocation(args.Scope.GoroutineID, args.Scope.Frame, args.Scope.DeferredCall, args.Loc, false, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type DisassembleRequest struct {
|
||||
Scope api.EvalScope
|
||||
StartPC, EndPC uint64
|
||||
Flavour api.AssemblyFlavour
|
||||
}
|
||||
|
||||
func (s *RPCServer) Disassemble(args DisassembleRequest, answer *api.AsmInstructions) error {
|
||||
var err error
|
||||
insts, err := s.debugger.Disassemble(args.Scope.GoroutineID, args.StartPC, args.EndPC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*answer = make(api.AsmInstructions, len(insts))
|
||||
for i := range insts {
|
||||
(*answer)[i] = api.ConvertAsmInstruction(insts[i], s.debugger.AsmInstructionText(&insts[i], proc.AssemblyFlavour(args.Flavour)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
package rpc2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"time"
|
||||
|
||||
"github.com/go-delve/delve/service"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
)
|
||||
|
||||
// RPCClient is a RPC service.Client.
|
||||
type RPCClient struct {
|
||||
client *rpc.Client
|
||||
|
||||
retValLoadCfg *api.LoadConfig
|
||||
}
|
||||
|
||||
// Ensure the implementation satisfies the interface.
|
||||
var _ service.Client = &RPCClient{}
|
||||
|
||||
// NewClient creates a new RPCClient.
|
||||
func NewClient(addr string) *RPCClient {
|
||||
client, err := jsonrpc.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal("dialing:", err)
|
||||
}
|
||||
return newFromRPCClient(client)
|
||||
}
|
||||
|
||||
func newFromRPCClient(client *rpc.Client) *RPCClient {
|
||||
c := &RPCClient{client: client}
|
||||
c.call("SetApiVersion", api.SetAPIVersionIn{APIVersion: 2}, &api.SetAPIVersionOut{})
|
||||
return c
|
||||
}
|
||||
|
||||
// NewClientFromConn creates a new RPCClient from the given connection.
|
||||
func NewClientFromConn(conn net.Conn) *RPCClient {
|
||||
return newFromRPCClient(jsonrpc.NewClient(conn))
|
||||
}
|
||||
|
||||
func (c *RPCClient) ProcessPid() int {
|
||||
out := new(ProcessPidOut)
|
||||
c.call("ProcessPid", ProcessPidIn{}, out)
|
||||
return out.Pid
|
||||
}
|
||||
|
||||
func (c *RPCClient) BuildID() string {
|
||||
out := new(BuildIDOut)
|
||||
c.call("BuildID", BuildIDIn{}, out)
|
||||
return out.BuildID
|
||||
}
|
||||
|
||||
func (c *RPCClient) LastModified() time.Time {
|
||||
out := new(LastModifiedOut)
|
||||
c.call("LastModified", LastModifiedIn{}, out)
|
||||
return out.Time
|
||||
}
|
||||
|
||||
func (c *RPCClient) Detach(kill bool) error {
|
||||
defer c.client.Close()
|
||||
out := new(DetachOut)
|
||||
return c.call("Detach", DetachIn{kill}, out)
|
||||
}
|
||||
|
||||
func (c *RPCClient) Restart(rebuild bool) ([]api.DiscardedBreakpoint, error) {
|
||||
out := new(RestartOut)
|
||||
err := c.call("Restart", RestartIn{"", false, nil, false, rebuild, [3]string{}}, out)
|
||||
return out.DiscardedBreakpoints, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
|
||||
out := new(RestartOut)
|
||||
err := c.call("Restart", RestartIn{pos, resetArgs, newArgs, rerecord, rebuild, newRedirects}, out)
|
||||
return out.DiscardedBreakpoints, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetState() (*api.DebuggerState, error) {
|
||||
var out StateOut
|
||||
err := c.call("State", StateIn{NonBlocking: false}, &out)
|
||||
return out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetStateNonBlocking() (*api.DebuggerState, error) {
|
||||
var out StateOut
|
||||
err := c.call("State", StateIn{NonBlocking: true}, &out)
|
||||
return out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Continue() <-chan *api.DebuggerState {
|
||||
return c.continueDir(api.Continue)
|
||||
}
|
||||
|
||||
func (c *RPCClient) Rewind() <-chan *api.DebuggerState {
|
||||
return c.continueDir(api.Rewind)
|
||||
}
|
||||
|
||||
func (c *RPCClient) DirectionCongruentContinue() <-chan *api.DebuggerState {
|
||||
return c.continueDir(api.DirectionCongruentContinue)
|
||||
}
|
||||
|
||||
func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState {
|
||||
ch := make(chan *api.DebuggerState)
|
||||
go func() {
|
||||
for {
|
||||
out := new(CommandOut)
|
||||
err := c.call("Command", &api.DebuggerCommand{Name: cmd, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
state := out.State
|
||||
if err != nil {
|
||||
state.Err = err
|
||||
}
|
||||
if state.Exited {
|
||||
// Error types apparently cannot be marshalled by Go correctly. Must reset error here.
|
||||
//lint:ignore ST1005 backwards compatibility
|
||||
state.Err = fmt.Errorf("Process %d has exited with status %d", c.ProcessPid(), state.ExitStatus)
|
||||
}
|
||||
ch <- &state
|
||||
if err != nil || state.Exited {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
isbreakpoint := false
|
||||
istracepoint := true
|
||||
for i := range state.Threads {
|
||||
if state.Threads[i].Breakpoint != nil {
|
||||
isbreakpoint = true
|
||||
istracepoint = istracepoint && (state.Threads[i].Breakpoint.Tracepoint || state.Threads[i].Breakpoint.TraceReturn)
|
||||
}
|
||||
}
|
||||
|
||||
if !isbreakpoint || !istracepoint {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *RPCClient) Next() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.Next, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ReverseNext() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseNext, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Step() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ReverseStep() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStep, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) StepOut() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ReverseStepOut() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Call(goroutineID int64, expr string, unsafe bool) (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) StepInstruction(skipCalls bool) (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
name := api.StepInstruction
|
||||
if skipCalls {
|
||||
name = api.NextInstruction
|
||||
}
|
||||
err := c.call("Command", api.DebuggerCommand{Name: name}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
name := api.ReverseStepInstruction
|
||||
if skipCalls {
|
||||
name = api.ReverseNextInstruction
|
||||
}
|
||||
err := c.call("Command", api.DebuggerCommand{Name: name}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SwitchThread(threadID int) (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
cmd := api.DebuggerCommand{
|
||||
Name: api.SwitchThread,
|
||||
ThreadID: threadID,
|
||||
}
|
||||
err := c.call("Command", cmd, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SwitchGoroutine(goroutineID int64) (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
cmd := api.DebuggerCommand{
|
||||
Name: api.SwitchGoroutine,
|
||||
GoroutineID: goroutineID,
|
||||
}
|
||||
err := c.call("Command", cmd, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Halt() (*api.DebuggerState, error) {
|
||||
var out CommandOut
|
||||
err := c.call("Command", api.DebuggerCommand{Name: api.Halt}, &out)
|
||||
return &out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetBufferedTracepoints() ([]api.TracepointResult, error) {
|
||||
var out GetBufferedTracepointsOut
|
||||
err := c.call("GetBufferedTracepoints", GetBufferedTracepointsIn{}, &out)
|
||||
return out.TracepointResults, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetBreakpoint(id int) (*api.Breakpoint, error) {
|
||||
var out GetBreakpointOut
|
||||
err := c.call("GetBreakpoint", GetBreakpointIn{id, ""}, &out)
|
||||
return &out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetBreakpointByName(name string) (*api.Breakpoint, error) {
|
||||
var out GetBreakpointOut
|
||||
err := c.call("GetBreakpoint", GetBreakpointIn{0, name}, &out)
|
||||
return &out.Breakpoint, err
|
||||
}
|
||||
|
||||
// CreateBreakpoint will send a request to the RPC server to create a breakpoint.
|
||||
// Please refer to the documentation for `Debugger.CreateBreakpoint` for a description of how
|
||||
// the requested breakpoint parameters are interpreted and used:
|
||||
// https://pkg.go.dev/github.com/go-delve/delve/service/debugger#Debugger.CreateBreakpoint
|
||||
func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) {
|
||||
var out CreateBreakpointOut
|
||||
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, "", nil, false}, &out)
|
||||
return &out.Breakpoint, err
|
||||
}
|
||||
|
||||
// CreateBreakpointWithExpr is like CreateBreakpoint but will also set a
|
||||
// location expression to be used to restore the breakpoint after it is
|
||||
// disabled.
|
||||
func (c *RPCClient) CreateBreakpointWithExpr(breakPoint *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
|
||||
var out CreateBreakpointOut
|
||||
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, locExpr, substitutePathRules, suspended}, &out)
|
||||
return &out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) CreateEBPFTracepoint(fnName string) error {
|
||||
var out CreateEBPFTracepointOut
|
||||
return c.call("CreateEBPFTracepoint", CreateEBPFTracepointIn{FunctionName: fnName}, &out)
|
||||
}
|
||||
|
||||
func (c *RPCClient) CreateWatchpoint(scope api.EvalScope, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
|
||||
var out CreateWatchpointOut
|
||||
err := c.call("CreateWatchpoint", CreateWatchpointIn{scope, expr, wtype}, &out)
|
||||
return out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListBreakpoints(all bool) ([]*api.Breakpoint, error) {
|
||||
var out ListBreakpointsOut
|
||||
err := c.call("ListBreakpoints", ListBreakpointsIn{all}, &out)
|
||||
return out.Breakpoints, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ClearBreakpoint(id int) (*api.Breakpoint, error) {
|
||||
var out ClearBreakpointOut
|
||||
err := c.call("ClearBreakpoint", ClearBreakpointIn{id, ""}, &out)
|
||||
return out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ClearBreakpointByName(name string) (*api.Breakpoint, error) {
|
||||
var out ClearBreakpointOut
|
||||
err := c.call("ClearBreakpoint", ClearBreakpointIn{0, name}, &out)
|
||||
return out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ToggleBreakpoint(id int) (*api.Breakpoint, error) {
|
||||
var out ToggleBreakpointOut
|
||||
err := c.call("ToggleBreakpoint", ToggleBreakpointIn{id, ""}, &out)
|
||||
return out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ToggleBreakpointByName(name string) (*api.Breakpoint, error) {
|
||||
var out ToggleBreakpointOut
|
||||
err := c.call("ToggleBreakpoint", ToggleBreakpointIn{0, name}, &out)
|
||||
return out.Breakpoint, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
|
||||
out := new(AmendBreakpointOut)
|
||||
err := c.call("AmendBreakpoint", AmendBreakpointIn{*bp}, out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *RPCClient) CancelNext() error {
|
||||
var out CancelNextOut
|
||||
return c.call("CancelNext", CancelNextIn{}, &out)
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
|
||||
var out ListThreadsOut
|
||||
err := c.call("ListThreads", ListThreadsIn{}, &out)
|
||||
return out.Threads, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetThread(id int) (*api.Thread, error) {
|
||||
var out GetThreadOut
|
||||
err := c.call("GetThread", GetThreadIn{id}, &out)
|
||||
return out.Thread, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) EvalVariable(scope api.EvalScope, expr string, cfg api.LoadConfig) (*api.Variable, error) {
|
||||
var out EvalOut
|
||||
err := c.call("Eval", EvalIn{scope, expr, &cfg}, &out)
|
||||
return out.Variable, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SetVariable(scope api.EvalScope, symbol, value string) error {
|
||||
out := new(SetOut)
|
||||
return c.call("Set", SetIn{scope, symbol, value}, out)
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListSources(filter string) ([]string, error) {
|
||||
sources := new(ListSourcesOut)
|
||||
err := c.call("ListSources", ListSourcesIn{filter}, sources)
|
||||
return sources.Sources, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListFunctions(filter string, TraceFollow int) ([]string, error) {
|
||||
funcs := new(ListFunctionsOut)
|
||||
err := c.call("ListFunctions", ListFunctionsIn{filter, TraceFollow}, funcs)
|
||||
return funcs.Funcs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListTypes(filter string) ([]string, error) {
|
||||
types := new(ListTypesOut)
|
||||
err := c.call("ListTypes", ListTypesIn{filter}, types)
|
||||
return types.Types, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListPackageVariables(filter string, cfg api.LoadConfig) ([]api.Variable, error) {
|
||||
var out ListPackageVarsOut
|
||||
err := c.call("ListPackageVars", ListPackageVarsIn{filter, cfg}, &out)
|
||||
return out.Variables, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListPackagesBuildInfo(filter string, includeFiles bool) ([]api.PackageBuildInfo, error) {
|
||||
var out ListPackagesBuildInfoOut
|
||||
err := c.call("ListPackagesBuildInfo", ListPackagesBuildInfoIn{Filter: filter, IncludeFiles: includeFiles}, &out)
|
||||
return out.List, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListLocalVariables(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) {
|
||||
var out ListLocalVarsOut
|
||||
err := c.call("ListLocalVars", ListLocalVarsIn{scope, cfg}, &out)
|
||||
return out.Variables, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListThreadRegisters(threadID int, includeFp bool) (api.Registers, error) {
|
||||
out := new(ListRegistersOut)
|
||||
err := c.call("ListRegisters", ListRegistersIn{ThreadID: threadID, IncludeFp: includeFp, Scope: nil}, out)
|
||||
return out.Regs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListScopeRegisters(scope api.EvalScope, includeFp bool) (api.Registers, error) {
|
||||
out := new(ListRegistersOut)
|
||||
err := c.call("ListRegisters", ListRegistersIn{ThreadID: 0, IncludeFp: includeFp, Scope: &scope}, out)
|
||||
return out.Regs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([]api.Variable, error) {
|
||||
var out ListFunctionArgsOut
|
||||
err := c.call("ListFunctionArgs", ListFunctionArgsIn{scope, cfg}, &out)
|
||||
return out.Args, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListGoroutines(start, count int) ([]*api.Goroutine, int, error) {
|
||||
var out ListGoroutinesOut
|
||||
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, nil, api.GoroutineGroupingOptions{}, nil}, &out)
|
||||
return out.Goroutines, out.Nextg, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions, scope *api.EvalScope) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error) {
|
||||
if group == nil {
|
||||
group = &api.GoroutineGroupingOptions{}
|
||||
}
|
||||
var out ListGoroutinesOut
|
||||
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, filters, *group, scope}, &out)
|
||||
return out.Goroutines, out.Groups, out.Nextg, out.TooManyGroups, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Stacktrace(goroutineId int64, depth int, opts api.StacktraceOptions, cfg *api.LoadConfig) ([]api.Stackframe, error) {
|
||||
var out StacktraceOut
|
||||
err := c.call("Stacktrace", StacktraceIn{goroutineId, depth, false, false, opts, cfg}, &out)
|
||||
return out.Locations, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) Ancestors(goroutineID int64, numAncestors int, depth int) ([]api.Ancestor, error) {
|
||||
var out AncestorsOut
|
||||
err := c.call("Ancestors", AncestorsIn{goroutineID, numAncestors, depth}, &out)
|
||||
return out.Ancestors, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) AttachedToExistingProcess() bool {
|
||||
out := new(AttachedToExistingProcessOut)
|
||||
c.call("AttachedToExistingProcess", AttachedToExistingProcessIn{}, out)
|
||||
return out.Answer
|
||||
}
|
||||
|
||||
func (c *RPCClient) FindLocation(scope api.EvalScope, loc string, findInstructions bool, substitutePathRules [][2]string) ([]api.Location, string, error) {
|
||||
var out FindLocationOut
|
||||
err := c.call("FindLocation", FindLocationIn{scope, loc, !findInstructions, substitutePathRules}, &out)
|
||||
return out.Locations, out.SubstituteLocExpr, err
|
||||
}
|
||||
|
||||
// DisassembleRange disassembles code between startPC and endPC
|
||||
func (c *RPCClient) DisassembleRange(scope api.EvalScope, startPC, endPC uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
|
||||
var out DisassembleOut
|
||||
err := c.call("Disassemble", DisassembleIn{scope, startPC, endPC, flavour}, &out)
|
||||
return out.Disassemble, err
|
||||
}
|
||||
|
||||
// DisassemblePC disassembles function containing pc
|
||||
func (c *RPCClient) DisassemblePC(scope api.EvalScope, pc uint64, flavour api.AssemblyFlavour) (api.AsmInstructions, error) {
|
||||
var out DisassembleOut
|
||||
err := c.call("Disassemble", DisassembleIn{scope, pc, 0, flavour}, &out)
|
||||
return out.Disassemble, err
|
||||
}
|
||||
|
||||
// Recorded returns true if the debugger target is a recording.
|
||||
func (c *RPCClient) Recorded() bool {
|
||||
out := new(RecordedOut)
|
||||
c.call("Recorded", RecordedIn{}, out)
|
||||
return out.Recorded
|
||||
}
|
||||
|
||||
// TraceDirectory returns the path to the trace directory for a recording.
|
||||
func (c *RPCClient) TraceDirectory() (string, error) {
|
||||
var out RecordedOut
|
||||
err := c.call("Recorded", RecordedIn{}, &out)
|
||||
return out.TraceDirectory, err
|
||||
}
|
||||
|
||||
// Checkpoint sets a checkpoint at the current position.
|
||||
func (c *RPCClient) Checkpoint(where string) (checkpointID int, err error) {
|
||||
var out CheckpointOut
|
||||
err = c.call("Checkpoint", CheckpointIn{where}, &out)
|
||||
return out.ID, err
|
||||
}
|
||||
|
||||
// ListCheckpoints gets all checkpoints.
|
||||
func (c *RPCClient) ListCheckpoints() ([]api.Checkpoint, error) {
|
||||
var out ListCheckpointsOut
|
||||
err := c.call("ListCheckpoints", ListCheckpointsIn{}, &out)
|
||||
return out.Checkpoints, err
|
||||
}
|
||||
|
||||
// ClearCheckpoint removes a checkpoint
|
||||
func (c *RPCClient) ClearCheckpoint(id int) error {
|
||||
var out ClearCheckpointOut
|
||||
err := c.call("ClearCheckpoint", ClearCheckpointIn{id}, &out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *RPCClient) SetReturnValuesLoadConfig(cfg *api.LoadConfig) {
|
||||
c.retValLoadCfg = cfg
|
||||
}
|
||||
|
||||
func (c *RPCClient) FunctionReturnLocations(fnName string) ([]uint64, error) {
|
||||
var out FunctionReturnLocationsOut
|
||||
err := c.call("FunctionReturnLocations", FunctionReturnLocationsIn{fnName}, &out)
|
||||
return out.Addrs, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) IsMulticlient() bool {
|
||||
var out IsMulticlientOut
|
||||
c.call("IsMulticlient", IsMulticlientIn{}, &out)
|
||||
return out.IsMulticlient
|
||||
}
|
||||
|
||||
func (c *RPCClient) Disconnect(cont bool) error {
|
||||
if cont {
|
||||
out := new(CommandOut)
|
||||
c.client.Go("RPCServer.Command", &api.DebuggerCommand{Name: api.Continue, ReturnInfoLoadConfig: c.retValLoadCfg}, &out, nil)
|
||||
}
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *RPCClient) ListDynamicLibraries() ([]api.Image, error) {
|
||||
var out ListDynamicLibrariesOut
|
||||
c.call("ListDynamicLibraries", ListDynamicLibrariesIn{}, &out)
|
||||
return out.List, nil
|
||||
}
|
||||
|
||||
func (c *RPCClient) ExamineMemory(address uint64, count int) ([]byte, bool, error) {
|
||||
out := &ExaminedMemoryOut{}
|
||||
|
||||
err := c.call("ExamineMemory", ExamineMemoryIn{Length: count, Address: address}, out)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return out.Mem, out.IsLittleEndian, nil
|
||||
}
|
||||
|
||||
func (c *RPCClient) StopRecording() error {
|
||||
return c.call("StopRecording", StopRecordingIn{}, &StopRecordingOut{})
|
||||
}
|
||||
|
||||
func (c *RPCClient) CoreDumpStart(dest string) (api.DumpState, error) {
|
||||
out := &DumpStartOut{}
|
||||
err := c.call("DumpStart", DumpStartIn{Destination: dest}, out)
|
||||
return out.State, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) CoreDumpWait(msec int) api.DumpState {
|
||||
out := &DumpWaitOut{}
|
||||
_ = c.call("DumpWait", DumpWaitIn{Wait: msec}, out)
|
||||
return out.State
|
||||
}
|
||||
|
||||
func (c *RPCClient) CoreDumpCancel() error {
|
||||
out := &DumpCancelOut{}
|
||||
return c.call("DumpCancel", DumpCancelIn{}, out)
|
||||
}
|
||||
|
||||
// ListTargets returns the current list of debug targets.
|
||||
func (c *RPCClient) ListTargets() ([]api.Target, error) {
|
||||
out := &ListTargetsOut{}
|
||||
err := c.call("ListTargets", ListTargetsIn{}, out)
|
||||
return out.Targets, err
|
||||
}
|
||||
|
||||
// FollowExec enabled or disabled follow exec mode. When follow exec is
|
||||
// enabled Delve will automatically attach to new subprocesses with a
|
||||
// command line matched by regex, if regex is nil all new subprocesses are
|
||||
// automatically debugged.
|
||||
func (c *RPCClient) FollowExec(v bool, regex string) error {
|
||||
out := &FollowExecOut{}
|
||||
err := c.call("FollowExec", FollowExecIn{Enable: v, Regex: regex}, out)
|
||||
return err
|
||||
}
|
||||
|
||||
// FollowExecEnabled returns true if follow exec mode is enabled.
|
||||
func (c *RPCClient) FollowExecEnabled() bool {
|
||||
out := &FollowExecEnabledOut{}
|
||||
_ = c.call("FollowExecEnabled", FollowExecEnabledIn{}, out)
|
||||
return out.Enabled
|
||||
}
|
||||
|
||||
func (c *RPCClient) SetDebugInfoDirectories(v []string) error {
|
||||
return c.call("DebugInfoDirectories", DebugInfoDirectoriesIn{Set: true, List: v}, &DebugInfoDirectoriesOut{})
|
||||
}
|
||||
|
||||
func (c *RPCClient) GetDebugInfoDirectories() ([]string, error) {
|
||||
out := &DebugInfoDirectoriesOut{}
|
||||
err := c.call("DebugInfoDirectories", DebugInfoDirectoriesIn{Set: false, List: nil}, out)
|
||||
return out.List, err
|
||||
}
|
||||
|
||||
func (c *RPCClient) call(method string, args, reply interface{}) error {
|
||||
return c.client.Call("RPCServer."+method, args, reply)
|
||||
}
|
||||
|
||||
func (c *RPCClient) CallAPI(method string, args, reply interface{}) error {
|
||||
return c.call(method, args, reply)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
package service
|
||||
|
||||
// RPCCallback is used by RPC methods to return their result asynchronously.
|
||||
type RPCCallback interface {
|
||||
Return(out interface{}, err error)
|
||||
|
||||
// SetupDoneChan returns a channel that should be closed to signal that the
|
||||
// asynchronous method has completed setup and the server is ready to
|
||||
// receive other requests.
|
||||
SetupDoneChan() chan struct{}
|
||||
|
||||
// DisconnectChan returns a channel that should be closed to signal that
|
||||
// the client that initially issued the command has been disconnected.
|
||||
DisconnectChan() chan struct{}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
package rpccommon
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"net/rpc/jsonrpc"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-delve/delve/pkg/logflags"
|
||||
"github.com/go-delve/delve/pkg/version"
|
||||
"github.com/go-delve/delve/service"
|
||||
"github.com/go-delve/delve/service/api"
|
||||
"github.com/go-delve/delve/service/dap"
|
||||
"github.com/go-delve/delve/service/debugger"
|
||||
"github.com/go-delve/delve/service/internal/sameuser"
|
||||
"github.com/go-delve/delve/service/rpc1"
|
||||
"github.com/go-delve/delve/service/rpc2"
|
||||
)
|
||||
|
||||
// ServerImpl implements a JSON-RPC server that can switch between two
|
||||
// versions of the API.
|
||||
type ServerImpl struct {
|
||||
// config is all the information necessary to start the debugger and server.
|
||||
config *service.Config
|
||||
// listener is used to serve JSON-RPC.
|
||||
listener net.Listener
|
||||
// stopChan is used to stop the listener goroutine.
|
||||
stopChan chan struct{}
|
||||
// debugger is the debugger service.
|
||||
debugger *debugger.Debugger
|
||||
// s1 is APIv1 server.
|
||||
s1 *rpc1.RPCServer
|
||||
// s2 is APIv2 server.
|
||||
s2 *rpc2.RPCServer
|
||||
// maps of served methods, one for each supported API.
|
||||
methodMaps []map[string]*methodType
|
||||
log logflags.Logger
|
||||
}
|
||||
|
||||
type RPCCallback struct {
|
||||
s *ServerImpl
|
||||
sending *sync.Mutex
|
||||
codec rpc.ServerCodec
|
||||
req rpc.Request
|
||||
setupDone chan struct{}
|
||||
disconnectChan chan struct{}
|
||||
}
|
||||
|
||||
var _ service.RPCCallback = &RPCCallback{}
|
||||
|
||||
// RPCServer implements the RPC method calls common to all versions of the API.
|
||||
type RPCServer struct {
|
||||
s *ServerImpl
|
||||
}
|
||||
|
||||
type methodType struct {
|
||||
method reflect.Method
|
||||
Rcvr reflect.Value
|
||||
ArgType reflect.Type
|
||||
ReplyType reflect.Type
|
||||
Synchronous bool
|
||||
}
|
||||
|
||||
// NewServer creates a new RPCServer.
|
||||
func NewServer(config *service.Config) *ServerImpl {
|
||||
logger := logflags.RPCLogger()
|
||||
if config.APIVersion < 2 {
|
||||
logger.Info("Using API v1")
|
||||
}
|
||||
if config.Debugger.Foreground {
|
||||
// Print listener address
|
||||
logflags.WriteAPIListeningMessage(config.Listener.Addr())
|
||||
logger.Debug("API server pid = ", os.Getpid())
|
||||
}
|
||||
return &ServerImpl{
|
||||
config: config,
|
||||
listener: config.Listener,
|
||||
stopChan: make(chan struct{}),
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the JSON-RPC server.
|
||||
func (s *ServerImpl) Stop() error {
|
||||
s.log.Debug("stopping")
|
||||
close(s.stopChan)
|
||||
if s.config.AcceptMulti {
|
||||
s.listener.Close()
|
||||
}
|
||||
if s.debugger.IsRunning() {
|
||||
s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil)
|
||||
}
|
||||
kill := s.config.Debugger.AttachPid == 0
|
||||
return s.debugger.Detach(kill)
|
||||
}
|
||||
|
||||
// Run starts a debugger and exposes it with an JSON-RPC server. The debugger
|
||||
// itself can be stopped with the `detach` API.
|
||||
func (s *ServerImpl) Run() error {
|
||||
var err error
|
||||
|
||||
if s.config.APIVersion < 2 {
|
||||
s.config.APIVersion = 1
|
||||
}
|
||||
if s.config.APIVersion > 2 {
|
||||
return errors.New("unknown API version")
|
||||
}
|
||||
|
||||
// Create and start the debugger
|
||||
config := s.config.Debugger
|
||||
if s.debugger, err = debugger.New(&config, s.config.ProcessArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.s1 = rpc1.NewServer(s.config, s.debugger)
|
||||
s.s2 = rpc2.NewServer(s.config, s.debugger)
|
||||
|
||||
rpcServer := &RPCServer{s}
|
||||
|
||||
s.methodMaps = make([]map[string]*methodType, 2)
|
||||
|
||||
s.methodMaps[0] = map[string]*methodType{}
|
||||
s.methodMaps[1] = map[string]*methodType{}
|
||||
suitableMethods(s.s1, s.methodMaps[0], s.log)
|
||||
suitableMethods(rpcServer, s.methodMaps[0], s.log)
|
||||
suitableMethods(s.s2, s.methodMaps[1], s.log)
|
||||
suitableMethods(rpcServer, s.methodMaps[1], s.log)
|
||||
|
||||
go func() {
|
||||
defer s.listener.Close()
|
||||
for {
|
||||
c, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
// We were supposed to exit, do nothing and return
|
||||
return
|
||||
default:
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.CheckLocalConnUser {
|
||||
if !sameuser.CanAccept(s.listener.Addr(), c.LocalAddr(), c.RemoteAddr()) {
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
go s.serveConnectionDemux(c)
|
||||
if !s.config.AcceptMulti {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type bufReadWriteCloser struct {
|
||||
*bufio.Reader
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
func (s *ServerImpl) serveConnectionDemux(c io.ReadWriteCloser) {
|
||||
conn := &bufReadWriteCloser{bufio.NewReader(c), c}
|
||||
b, err := conn.Peek(1)
|
||||
if err != nil {
|
||||
s.log.Warnf("error determining new connection protocol: %v", err)
|
||||
return
|
||||
}
|
||||
if b[0] == 'C' { // C is for DAP's Content-Length
|
||||
s.log.Debugf("serving DAP on new connection")
|
||||
ds := dap.NewSession(conn, &dap.Config{Config: s.config, StopTriggered: s.stopChan}, s.debugger)
|
||||
go ds.ServeDAPCodec()
|
||||
} else {
|
||||
s.log.Debugf("serving JSON-RPC on new connection")
|
||||
go s.serveJSONCodec(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Precompute the reflect type for error. Can't use error directly
|
||||
// because Typeof takes an empty interface value. This is annoying.
|
||||
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
// Is this an exported - upper case - name?
|
||||
func isExported(name string) bool {
|
||||
ch, _ := utf8.DecodeRuneInString(name)
|
||||
return unicode.IsUpper(ch)
|
||||
}
|
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
return isExported(t.Name()) || t.PkgPath() == ""
|
||||
}
|
||||
|
||||
// Fills methods map with the methods of receiver that should be made
|
||||
// available through the RPC interface.
|
||||
// These are all the public methods of rcvr that have one of those
|
||||
// two signatures:
|
||||
//
|
||||
// func (rcvr ReceiverType) Method(in InputType, out *ReplyType) error
|
||||
// func (rcvr ReceiverType) Method(in InputType, cb service.RPCCallback)
|
||||
func suitableMethods(rcvr interface{}, methods map[string]*methodType, log logflags.Logger) {
|
||||
typ := reflect.TypeOf(rcvr)
|
||||
rcvrv := reflect.ValueOf(rcvr)
|
||||
sname := reflect.Indirect(rcvrv).Type().Name()
|
||||
if sname == "" {
|
||||
log.Debugf("rpc.Register: no service name for type %s", typ)
|
||||
return
|
||||
}
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
method := typ.Method(m)
|
||||
mname := method.Name
|
||||
mtype := method.Type
|
||||
// method must be exported
|
||||
if method.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
// Method needs three ins: (receive, *args, *reply) or (receiver, *args, *RPCCallback)
|
||||
if mtype.NumIn() != 3 {
|
||||
log.Warn("method", mname, "has wrong number of ins:", mtype.NumIn())
|
||||
continue
|
||||
}
|
||||
// First arg need not be a pointer.
|
||||
argType := mtype.In(1)
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
log.Warn(mname, "argument type not exported:", argType)
|
||||
continue
|
||||
}
|
||||
|
||||
replyType := mtype.In(2)
|
||||
synchronous := replyType.String() != "service.RPCCallback"
|
||||
|
||||
if synchronous {
|
||||
// Second arg must be a pointer.
|
||||
if replyType.Kind() != reflect.Ptr {
|
||||
log.Warn("method", mname, "reply type not a pointer:", replyType)
|
||||
continue
|
||||
}
|
||||
// Reply type must be exported.
|
||||
if !isExportedOrBuiltinType(replyType) {
|
||||
log.Warn("method", mname, "reply type not exported:", replyType)
|
||||
continue
|
||||
}
|
||||
|
||||
// Method needs one out.
|
||||
if mtype.NumOut() != 1 {
|
||||
log.Warn("method", mname, "has wrong number of outs:", mtype.NumOut())
|
||||
continue
|
||||
}
|
||||
// The return type of the method must be error.
|
||||
if returnType := mtype.Out(0); returnType != typeOfError {
|
||||
log.Warn("method", mname, "returns", returnType.String(), "not error")
|
||||
continue
|
||||
}
|
||||
} else if mtype.NumOut() != 0 {
|
||||
// Method needs zero outs.
|
||||
log.Warn("method", mname, "has wrong number of outs:", mtype.NumOut())
|
||||
continue
|
||||
}
|
||||
methods[sname+"."+mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType, Synchronous: synchronous, Rcvr: rcvrv}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) {
|
||||
clientDisconnectChan := make(chan struct{})
|
||||
defer func() {
|
||||
close(clientDisconnectChan)
|
||||
if !s.config.AcceptMulti && s.config.DisconnectChan != nil {
|
||||
close(s.config.DisconnectChan)
|
||||
}
|
||||
}()
|
||||
|
||||
sending := new(sync.Mutex)
|
||||
codec := jsonrpc.NewServerCodec(conn)
|
||||
var req rpc.Request
|
||||
var resp rpc.Response
|
||||
for {
|
||||
req = rpc.Request{}
|
||||
err := codec.ReadRequestHeader(&req)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
s.log.Error("rpc:", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
mtype, ok := s.methodMaps[s.config.APIVersion-1][req.ServiceMethod]
|
||||
if !ok {
|
||||
s.log.Errorf("rpc: can't find method %s", req.ServiceMethod)
|
||||
s.sendResponse(sending, &req, &rpc.Response{}, nil, codec, fmt.Sprintf("unknown method: %s", req.ServiceMethod))
|
||||
continue
|
||||
}
|
||||
|
||||
var argv, replyv reflect.Value
|
||||
|
||||
// Decode the argument value.
|
||||
argIsValue := false // if true, need to indirect before calling.
|
||||
if mtype.ArgType.Kind() == reflect.Ptr {
|
||||
argv = reflect.New(mtype.ArgType.Elem())
|
||||
} else {
|
||||
argv = reflect.New(mtype.ArgType)
|
||||
argIsValue = true
|
||||
}
|
||||
// argv guaranteed to be a pointer now.
|
||||
if err = codec.ReadRequestBody(argv.Interface()); err != nil {
|
||||
return
|
||||
}
|
||||
if argIsValue {
|
||||
argv = argv.Elem()
|
||||
}
|
||||
|
||||
if mtype.Synchronous {
|
||||
if logflags.RPC() {
|
||||
argvbytes, _ := json.Marshal(argv.Interface())
|
||||
s.log.Debugf("<- %s(%T%s)", req.ServiceMethod, argv.Interface(), argvbytes)
|
||||
}
|
||||
replyv = reflect.New(mtype.ReplyType.Elem())
|
||||
function := mtype.method.Func
|
||||
var returnValues []reflect.Value
|
||||
var errInter interface{}
|
||||
func() {
|
||||
defer func() {
|
||||
if ierr := recover(); ierr != nil {
|
||||
errInter = newInternalError(ierr, 2)
|
||||
}
|
||||
}()
|
||||
returnValues = function.Call([]reflect.Value{mtype.Rcvr, argv, replyv})
|
||||
errInter = returnValues[0].Interface()
|
||||
}()
|
||||
|
||||
errmsg := ""
|
||||
if errInter != nil {
|
||||
errmsg = errInter.(error).Error()
|
||||
}
|
||||
resp = rpc.Response{}
|
||||
if logflags.RPC() {
|
||||
replyvbytes, _ := json.Marshal(replyv.Interface())
|
||||
s.log.Debugf("-> %T%s error: %q", replyv.Interface(), replyvbytes, errmsg)
|
||||
}
|
||||
s.sendResponse(sending, &req, &resp, replyv.Interface(), codec, errmsg)
|
||||
if req.ServiceMethod == "RPCServer.Detach" && s.config.DisconnectChan != nil {
|
||||
close(s.config.DisconnectChan)
|
||||
s.config.DisconnectChan = nil
|
||||
}
|
||||
} else {
|
||||
if logflags.RPC() {
|
||||
argvbytes, _ := json.Marshal(argv.Interface())
|
||||
s.log.Debugf("(async %d) <- %s(%T%s)", req.Seq, req.ServiceMethod, argv.Interface(), argvbytes)
|
||||
}
|
||||
function := mtype.method.Func
|
||||
ctl := &RPCCallback{s, sending, codec, req, make(chan struct{}), clientDisconnectChan}
|
||||
go func() {
|
||||
defer func() {
|
||||
if ierr := recover(); ierr != nil {
|
||||
ctl.Return(nil, newInternalError(ierr, 2))
|
||||
}
|
||||
}()
|
||||
function.Call([]reflect.Value{mtype.Rcvr, argv, reflect.ValueOf(ctl)})
|
||||
}()
|
||||
<-ctl.setupDone
|
||||
}
|
||||
}
|
||||
codec.Close()
|
||||
}
|
||||
|
||||
// A value sent as a placeholder for the server's response value when the server
|
||||
// receives an invalid request. It is never decoded by the client since the Response
|
||||
// contains an error when it is used.
|
||||
var invalidRequest = struct{}{}
|
||||
|
||||
func (s *ServerImpl) sendResponse(sending *sync.Mutex, req *rpc.Request, resp *rpc.Response, reply interface{}, codec rpc.ServerCodec, errmsg string) {
|
||||
resp.ServiceMethod = req.ServiceMethod
|
||||
if errmsg != "" {
|
||||
resp.Error = errmsg
|
||||
reply = invalidRequest
|
||||
}
|
||||
resp.Seq = req.Seq
|
||||
sending.Lock()
|
||||
defer sending.Unlock()
|
||||
err := codec.WriteResponse(resp, reply)
|
||||
if err != nil {
|
||||
s.log.Error("writing response:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (cb *RPCCallback) Return(out interface{}, err error) {
|
||||
select {
|
||||
case <-cb.setupDone:
|
||||
// already closed
|
||||
default:
|
||||
close(cb.setupDone)
|
||||
}
|
||||
errmsg := ""
|
||||
if err != nil {
|
||||
errmsg = err.Error()
|
||||
}
|
||||
var resp rpc.Response
|
||||
if logflags.RPC() {
|
||||
outbytes, _ := json.Marshal(out)
|
||||
cb.s.log.Debugf("(async %d) -> %T%s error: %q", cb.req.Seq, out, outbytes, errmsg)
|
||||
}
|
||||
|
||||
if cb.hasDisconnected() {
|
||||
return
|
||||
}
|
||||
|
||||
cb.s.sendResponse(cb.sending, &cb.req, &resp, out, cb.codec, errmsg)
|
||||
}
|
||||
|
||||
func (cb *RPCCallback) DisconnectChan() chan struct{} {
|
||||
return cb.disconnectChan
|
||||
}
|
||||
|
||||
func (cb *RPCCallback) hasDisconnected() bool {
|
||||
select {
|
||||
case <-cb.disconnectChan:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cb *RPCCallback) SetupDoneChan() chan struct{} {
|
||||
return cb.setupDone
|
||||
}
|
||||
|
||||
// GetVersion returns the version of delve as well as the API version
|
||||
// currently served.
|
||||
func (s *RPCServer) GetVersion(args api.GetVersionIn, out *api.GetVersionOut) error {
|
||||
out.DelveVersion = version.DelveVersion.String()
|
||||
out.APIVersion = s.s.config.APIVersion
|
||||
return s.s.debugger.GetVersion(out)
|
||||
}
|
||||
|
||||
// SetApiVersion changes version of the API being served.
|
||||
func (s *RPCServer) SetApiVersion(args api.SetAPIVersionIn, out *api.SetAPIVersionOut) error {
|
||||
if args.APIVersion < 2 {
|
||||
args.APIVersion = 1
|
||||
}
|
||||
if args.APIVersion > 2 {
|
||||
return errors.New("unknown API version")
|
||||
}
|
||||
s.s.config.APIVersion = args.APIVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
type internalError struct {
|
||||
Err interface{}
|
||||
Stack []internalErrorFrame
|
||||
}
|
||||
|
||||
type internalErrorFrame struct {
|
||||
Pc uintptr
|
||||
Func string
|
||||
File string
|
||||
Line int
|
||||
}
|
||||
|
||||
func newInternalError(ierr interface{}, skip int) *internalError {
|
||||
r := &internalError{ierr, nil}
|
||||
for i := skip; ; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
fname := "<unknown>"
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn != nil {
|
||||
fname = fn.Name()
|
||||
}
|
||||
r.Stack = append(r.Stack, internalErrorFrame{pc, fname, file, line})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (err *internalError) Error() string {
|
||||
var out bytes.Buffer
|
||||
fmt.Fprintf(&out, "Internal debugger error: %v\n", err.Err)
|
||||
for _, frame := range err.Stack {
|
||||
fmt.Fprintf(&out, "%s (%#x)\n\t%s:%d\n", frame.Func, frame.Pc, frame.File, frame.Line)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package service
|
||||
|
||||
// Server represents a server for a remote client
|
||||
// to connect to.
|
||||
type Server interface {
|
||||
Run() error
|
||||
Stop() error
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-delve/delve/service/api"
|
||||
"github.com/go-delve/delve/service/rpc1"
|
||||
"github.com/go-delve/delve/service/rpc2"
|
||||
)
|
||||
|
||||
func assertNoError(err error, t *testing.T, s string) {
|
||||
if err != nil {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fname := filepath.Base(file)
|
||||
t.Fatalf("failed assertion at %s:%d: %s - %s\n", fname, line, s, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertError(err error, t *testing.T, s string) {
|
||||
if err == nil {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fname := filepath.Base(file)
|
||||
t.Fatalf("failed assertion at %s:%d: %s (no error)\n", fname, line, s)
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "Internal debugger error") {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
fname := filepath.Base(file)
|
||||
t.Fatalf("failed assertion at %s:%d: %s internal debugger error: %v\n", fname, line, s, err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
runtime.GOMAXPROCS(2)
|
||||
}
|
||||
|
||||
type nextTest struct {
|
||||
begin, end int
|
||||
}
|
||||
|
||||
func testProgPath(t *testing.T, name string) string {
|
||||
fp, err := filepath.Abs(fmt.Sprintf("_fixtures/%s.go", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(fp); err != nil {
|
||||
fp, err = filepath.Abs(fmt.Sprintf("../../_fixtures/%s.go", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
sympath, err := filepath.EvalSymlinks(fp)
|
||||
if err == nil {
|
||||
fp = strings.ReplaceAll(sympath, "\\", "/")
|
||||
}
|
||||
return fp
|
||||
}
|
||||
|
||||
type BreakpointLister interface {
|
||||
ListBreakpoints() ([]*api.Breakpoint, error)
|
||||
}
|
||||
|
||||
func countBreakpoints(t *testing.T, c interface{}) int {
|
||||
var bps []*api.Breakpoint
|
||||
var err error
|
||||
switch c := c.(type) {
|
||||
case *rpc2.RPCClient:
|
||||
bps, err = c.ListBreakpoints(false)
|
||||
case *rpc1.RPCClient:
|
||||
bps, err = c.ListBreakpoints()
|
||||
}
|
||||
assertNoError(err, t, "ListBreakpoints()")
|
||||
bpcount := 0
|
||||
for _, bp := range bps {
|
||||
if bp.ID >= 0 {
|
||||
bpcount++
|
||||
}
|
||||
}
|
||||
return bpcount
|
||||
}
|
||||
|
||||
type locationFinder1 interface {
|
||||
FindLocation(api.EvalScope, string) ([]api.Location, error)
|
||||
}
|
||||
|
||||
type locationFinder2 interface {
|
||||
FindLocation(api.EvalScope, string, bool, [][2]string) ([]api.Location, string, error)
|
||||
}
|
||||
|
||||
func findLocationHelper(t *testing.T, c interface{}, loc string, shouldErr bool, count int, checkAddr uint64) []uint64 {
|
||||
var locs []api.Location
|
||||
var err error
|
||||
|
||||
switch c := c.(type) {
|
||||
case locationFinder1:
|
||||
locs, err = c.FindLocation(api.EvalScope{GoroutineID: -1}, loc)
|
||||
case locationFinder2:
|
||||
locs, _, err = c.FindLocation(api.EvalScope{GoroutineID: -1}, loc, false, nil)
|
||||
default:
|
||||
t.Errorf("unexpected type %T passed to findLocationHelper", c)
|
||||
}
|
||||
|
||||
t.Logf("FindLocation(\"%s\") → %v\n", loc, locs)
|
||||
|
||||
if shouldErr {
|
||||
if err == nil {
|
||||
t.Fatalf("Resolving location <%s> didn't return an error: %v", loc, locs)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Error resolving location <%s>: %v", loc, err)
|
||||
}
|
||||
}
|
||||
|
||||
if (count >= 0) && (len(locs) != count) {
|
||||
t.Fatalf("Wrong number of breakpoints returned for location <%s> (got %d, expected %d)", loc, len(locs), count)
|
||||
}
|
||||
|
||||
if checkAddr != 0 && checkAddr != locs[0].PC {
|
||||
t.Fatalf("Wrong address returned for location <%s> (got %#x, expected %#x)", loc, locs[0].PC, checkAddr)
|
||||
}
|
||||
|
||||
addrs := make([]uint64, len(locs))
|
||||
for i := range locs {
|
||||
addrs[i] = locs[i].PC
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
func getCurinstr(d3 api.AsmInstructions) *api.AsmInstruction {
|
||||
for i := range d3 {
|
||||
if d3[i].AtPC {
|
||||
return &d3[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user