From a8606afb0b71b7726edd4b6159f18a672c5903de Mon Sep 17 00:00:00 2001 From: aarzilli Date: Tue, 3 Dec 2019 14:00:30 +0100 Subject: [PATCH] proc,service: return build informations for each package Adds an API call that returns a list of packages contained in the program and the files that were used to build them, and also a best guess at which filesystem directory contained the package when it was built. This can be used by IDEs to map file paths if the debugging environment doesn't match the build environment exactly. --- Documentation/cli/starlark.md | 1 + pkg/dwarf/line/state_machine.go | 17 ++++++- pkg/proc/bininfo.go | 57 +++++++++++++++++++++-- pkg/proc/proc_test.go | 21 +++++++++ pkg/terminal/starbind/starlark_mapping.go | 30 ++++++++++++ service/api/types.go | 7 +++ service/debugger/debugger.go | 29 ++++++++++++ service/rpc2/server.go | 20 ++++++++ 8 files changed, 177 insertions(+), 5 deletions(-) diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index f163eac1..6e173f30 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -43,6 +43,7 @@ functions(Filter) | Equivalent to API call [ListFunctions](https://godoc.org/git goroutines(Start, Count) | Equivalent to API call [ListGoroutines](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListGoroutines) local_vars(Scope, Cfg) | Equivalent to API call [ListLocalVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListLocalVars) package_vars(Filter, Cfg) | Equivalent to API call [ListPackageVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackageVars) +packages_build_info(IncludeFiles) | Equivalent to API call [ListPackagesBuildInfo](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackagesBuildInfo) registers(ThreadID, IncludeFp) | Equivalent to API call [ListRegisters](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListRegisters) sources(Filter) | Equivalent to API call [ListSources](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListSources) threads() | Equivalent to API call [ListThreads](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListThreads) diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go index d83d1422..cb041544 100644 --- a/pkg/dwarf/line/state_machine.go +++ b/pkg/dwarf/line/state_machine.go @@ -385,13 +385,28 @@ func (lineInfo *DebugLineInfo) FirstStmtForLine(start, end uint64) (pc uint64, f } if err := sm.next(); err != nil { if lineInfo.Logf != nil { - lineInfo.Logf("StmtAfter error: %v", err) + lineInfo.Logf("FirstStmtForLine error: %v", err) } return 0, "", 0, false } } } +func (lineInfo *DebugLineInfo) FirstFile() string { + sm := newStateMachine(lineInfo, lineInfo.Instructions) + for { + if sm.valid { + return sm.file + } + if err := sm.next(); err != nil { + if lineInfo.Logf != nil { + lineInfo.Logf("FirstFile error: %v", err) + } + return "" + } + } +} + func (sm *StateMachine) next() error { sm.started = true if sm.valid { diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 4cf1fd62..732505aa 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -351,7 +351,7 @@ func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pcs []uint64, err e fileFound := false var pc uint64 for _, cu := range bi.compileUnits { - if cu.lineInfo.Lookup[filename] == nil { + if cu.lineInfo != nil && cu.lineInfo.Lookup[filename] == nil { continue } fileFound = true @@ -411,7 +411,7 @@ func (bi *BinaryInfo) AllPCsForFileLines(filename string, linenos []int) map[int r[line] = make([]uint64, 0, 1) } for _, cu := range bi.compileUnits { - if cu.lineInfo.Lookup[filename] != nil { + if cu.lineInfo != nil && cu.lineInfo.Lookup[filename] != nil { cu.lineInfo.AllPCsForFileLines(filename, r) } } @@ -1317,8 +1317,8 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugLineBytes []byte, wg if len(cu.ranges) >= 1 { cu.lowPC = cu.ranges[0][0] } - lineInfoOffset, _ := entry.Val(dwarf.AttrStmtList).(int64) - if lineInfoOffset >= 0 && lineInfoOffset < int64(len(debugLineBytes)) { + lineInfoOffset, hasLineInfo := entry.Val(dwarf.AttrStmtList).(int64) + if hasLineInfo && lineInfoOffset >= 0 && lineInfoOffset < int64(len(debugLineBytes)) { var logfn func(string, ...interface{}) if logflags.DebugLineErrors() { logger := logrus.New().WithFields(logrus.Fields{"layer": "dwarf-line"}) @@ -1756,3 +1756,52 @@ func (bi *BinaryInfo) symLookup(addr uint64) (string, uint64) { } return "", 0 } + +type PackageBuildInfo struct { + ImportPath string + DirectoryPath string + Files map[string]struct{} +} + +// ListPackagesBuildInfo returns the list of packages used by the program along with +// the directory where each package was compiled and optionally the list of +// files constituting the package. +func (bi *BinaryInfo) ListPackagesBuildInfo(includeFiles bool) []*PackageBuildInfo { + m := make(map[string]*PackageBuildInfo) + for _, cu := range bi.compileUnits { + if cu.image != bi.Images[0] || !cu.isgo || cu.lineInfo == nil { + //TODO(aarzilli): what's the correct thing to do for plugins? + continue + } + + ip := strings.Replace(cu.name, "\\", "/", -1) + if _, ok := m[ip]; !ok { + path := cu.lineInfo.FirstFile() + if ext := filepath.Ext(path); ext != ".go" && ext != ".s" { + continue + } + dp := filepath.Dir(path) + m[ip] = &PackageBuildInfo{ + ImportPath: ip, + DirectoryPath: dp, + Files: make(map[string]struct{}), + } + } + + if includeFiles { + pbi := m[ip] + + for _, file := range cu.lineInfo.FileNames { + pbi.Files[file.Path] = struct{}{} + } + } + } + + r := make([]*PackageBuildInfo, 0, len(m)) + for _, pbi := range m { + r = append(r, pbi) + } + + sort.Slice(r, func(i, j int) bool { return r[i].ImportPath < r[j].ImportPath }) + return r +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index bd8c2ccd..4f9282a1 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -4525,3 +4525,24 @@ func TestIssue1817(t *testing.T) { setFileBreakpoint(p, t, fixture.Source, 16) }) } + +func TestListPackagesBuildInfo(t *testing.T) { + withTestProcess("pkgrenames", t, func(p proc.Process, fixture protest.Fixture) { + pkgs := p.BinInfo().ListPackagesBuildInfo(true) + t.Logf("returned %d", len(pkgs)) + if len(pkgs) < 10 { + t.Errorf("very few packages returned") + } + for _, pkg := range pkgs { + t.Logf("%q %q", pkg.ImportPath, pkg.DirectoryPath) + const _fixtures = "_fixtures" + fidx := strings.Index(pkg.ImportPath, _fixtures) + if fidx < 0 { + continue + } + if !strings.HasSuffix(strings.Replace(pkg.DirectoryPath, "\\", "/", -1), pkg.ImportPath[fidx:]) { + t.Errorf("unexpected suffix: %q %q", pkg.ImportPath, pkg.DirectoryPath) + } + } + }) +} diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 3cae3aca..f00059a4 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -848,6 +848,36 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { } return env.interfaceToStarlarkValue(rpcRet), nil }) + r["packages_build_info"] = starlark.NewBuiltin("packages_build_info", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.ListPackagesBuildInfoIn + var rpcRet rpc2.ListPackagesBuildInfoOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.IncludeFiles, "IncludeFiles") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "IncludeFiles": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.IncludeFiles, "IncludeFiles") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("ListPackagesBuildInfo", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) r["registers"] = starlark.NewBuiltin("registers", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) diff --git a/service/api/types.go b/service/api/types.go index 33e0dffa..db06c7b8 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -509,3 +509,10 @@ const ( // values saved in the runtime.g structure. StacktraceG ) + +// ImportPathToDirectoryPath maps an import path to a directory path. +type PackageBuildInfo struct { + ImportPath string + DirectoryPath string + Files []string +} diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 7eafe5d7..56ec1645 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1337,6 +1337,35 @@ func (d *Debugger) GetVersion(out *api.GetVersionOut) error { return nil } +// ListPackagesBuildInfo returns the list of packages used by the program along with +// the directory where each package was compiled and optionally the list of +// files constituting the package. +func (d *Debugger) ListPackagesBuildInfo(includeFiles bool) []api.PackageBuildInfo { + d.processMutex.Lock() + defer d.processMutex.Unlock() + pkgs := d.target.BinInfo().ListPackagesBuildInfo(includeFiles) + r := make([]api.PackageBuildInfo, 0, len(pkgs)) + for _, pkg := range pkgs { + var files []string + + if len(pkg.Files) > 0 { + files = make([]string, 0, len(pkg.Files)) + for file := range pkg.Files { + files = append(files, file) + } + } + + sort.Strings(files) + + r = append(r, api.PackageBuildInfo{ + ImportPath: pkg.ImportPath, + DirectoryPath: pkg.DirectoryPath, + Files: files, + }) + } + return r +} + func go11DecodeErrorCheck(err error) error { if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr { return err diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 730f1e9c..68b84c6d 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -731,3 +731,23 @@ func (s *RPCServer) ListDynamicLibraries(in ListDynamicLibrariesIn, out *ListDyn out.List = s.debugger.ListDynamicLibraries() return nil } + +// ListPackagesBuildInfoIn holds the arguments of ListPackages. +type ListPackagesBuildInfoIn struct { + IncludeFiles bool +} + +// ListPackagesBuildInfoOut holds the return values of ListPackages. +type ListPackagesBuildInfoOut struct { + List []api.PackageBuildInfo +} + +// ListPackagesBuildInfo returns the list of packages used by the program along with +// the directory where each package was compiled and optionally the list of +// files constituting the package. +// Note that the directory path is a best guess and may be wrong is a tool +// other than cmd/go is used to perform the build. +func (s *RPCServer) ListPackagesBuildInfo(in ListPackagesBuildInfoIn, out *ListPackagesBuildInfoOut) error { + out.List = s.debugger.ListPackagesBuildInfo(in.IncludeFiles) + return nil +}