From f17d6878ae4a8b54265624e9bc7545a7c222d415 Mon Sep 17 00:00:00 2001 From: aarzilli Date: Thu, 11 Jul 2024 13:56:53 +0200 Subject: [PATCH] service,terminal,cmd/dlv: automatically guessing substitute-path config Add command, API calls and launch.json option to automatically guess substitute-path configuration. --- Documentation/cli/README.md | 3 + Documentation/cli/starlark.md | 1 + .../usage/dlv_substitute-path-guess-helper.md | 39 +++++ _scripts/make.go | 5 +- cmd/dlv/cmds/commands.go | 18 ++ cmd/dlv/dlv_test.go | 52 +----- pkg/proc/bininfo.go | 7 + pkg/proc/test/support.go | 34 ++++ pkg/terminal/command.go | 3 + pkg/terminal/config.go | 11 ++ pkg/terminal/starbind/starlark_mapping.go | 31 ++++ service/api/types.go | 7 + service/client.go | 3 + service/dap/server.go | 12 ++ service/dap/types.go | 7 + service/debugger/debugger.go | 155 ++++++++++++++++++ service/debugger/debugger_test.go | 53 ++++++ service/rpc2/client.go | 85 ++++++++++ service/rpc2/server.go | 16 ++ service/test/integration2_test.go | 129 +++++++++++++++ 20 files changed, 627 insertions(+), 44 deletions(-) create mode 100644 Documentation/usage/dlv_substitute-path-guess-helper.md diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 7cf266dd0b..4efd68c078 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -256,10 +256,13 @@ Changes the value of a configuration parameter. config substitute-path config substitute-path config substitute-path -clear + config substitute-path -guess Adds or removes a path substitution rule, if -clear is used all substitute-path rules are removed. Without arguments shows the current list of substitute-path rules. +The -guess option causes Delve to try to guess your substitute-path +configuration automatically. See also [Documentation/cli/substitutepath.md](//github.com/go-delve/delve/tree/master/Documentation/cli/substitutepath.md) for how the rules are applied. config alias diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index edb878dd35..006a00642e 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -44,6 +44,7 @@ function_return_locations(FnName) | Equivalent to API call [FunctionReturnLocati get_breakpoint(Id, Name) | Equivalent to API call [GetBreakpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetBreakpoint) get_buffered_tracepoints() | Equivalent to API call [GetBufferedTracepoints](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetBufferedTracepoints) get_thread(Id) | Equivalent to API call [GetThread](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetThread) +guess_substitute_path(Args) | Equivalent to API call [GuessSubstitutePath](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GuessSubstitutePath) is_multiclient() | Equivalent to API call [IsMulticlient](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.IsMulticlient) last_modified() | Equivalent to API call [LastModified](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.LastModified) breakpoints(All) | Equivalent to API call [ListBreakpoints](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.ListBreakpoints) diff --git a/Documentation/usage/dlv_substitute-path-guess-helper.md b/Documentation/usage/dlv_substitute-path-guess-helper.md new file mode 100644 index 0000000000..4a0c1ba548 --- /dev/null +++ b/Documentation/usage/dlv_substitute-path-guess-helper.md @@ -0,0 +1,39 @@ +## dlv substitute-path-guess-helper + + + +``` +dlv substitute-path-guess-helper [flags] +``` + +### Options + +``` + -h, --help help for substitute-path-guess-helper +``` + +### Options inherited from parent commands + +``` + --accept-multiclient Allows a headless server to accept multiple client connections via JSON-RPC or DAP. + --allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr + --api-version int Selects JSON-RPC API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1) + --backend string Backend selection (see 'dlv help backend'). (default "default") + --build-flags string Build flags, to be passed to the compiler. For example: --build-flags="-tags=integration -mod=vendor -cover -v" + --check-go-version Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true) + --disable-aslr Disables address space randomization + --headless Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections. + --init string Init file, executed by the terminal client. + -l, --listen string Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0") + --log Enable debugging server logging. + --log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log'). + --log-output string Comma separated list of components that should produce debug output (see 'dlv help log') + --only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true) + -r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect') + --wd string Working directory for running the program. +``` + +### SEE ALSO + +* [dlv](dlv.md) - Delve is a debugger for the Go programming language. + diff --git a/_scripts/make.go b/_scripts/make.go index 2a7442faf6..78d99440da 100644 --- a/_scripts/make.go +++ b/_scripts/make.go @@ -130,6 +130,9 @@ This option can only be specified if testset is basic or a single package.`) } func checkCert() bool { + if os.Getenv("NOCERT") != "" { + return false + } // If we're on OSX make sure the proper CERT env var is set. if runtime.GOOS != "darwin" || os.Getenv("CERT") != "" { return true @@ -321,7 +324,7 @@ func buildFlags() []string { } else { ldFlags = "-X main.Build=" + buildSHA } - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" && os.Getenv("CERT") != "" { ldFlags = "-s " + ldFlags } return []string{fmt.Sprintf("-ldflags=%s", ldFlags)} diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 95b294f040..06df0b078d 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -512,6 +512,24 @@ File redirects can also be changed using the 'restart' command. `, }) + rootCommand.AddCommand(&cobra.Command{ + Use: "substitute-path-guess-helper", + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + gsp, err := rpc2.MakeGuessSusbtitutePathIn() + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + err = json.NewEncoder(os.Stdout).Encode(gsp) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + os.Exit(0) + }, + }) + rootCommand.DisableAutoGenTag = true configUsageFunc(rootCommand) diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index c9fd264102..ebad05e82f 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -61,45 +61,18 @@ func assertNoError(err error, t testing.TB, s string) { } } -func projectRoot() string { - wd, err := os.Getwd() - if err != nil { - panic(err) - } - - gopaths := strings.FieldsFunc(os.Getenv("GOPATH"), func(r rune) bool { return r == os.PathListSeparator }) - for _, curpath := range gopaths { - // Detects "gopath mode" when GOPATH contains several paths ex. "d:\\dir\\gopath;f:\\dir\\gopath2" - if strings.Contains(wd, curpath) { - return filepath.Join(curpath, "src", "github.com", "go-delve", "delve") - } - } - val, err := exec.Command("go", "list", "-mod=", "-m", "-f", "{{ .Dir }}").Output() - if err != nil { - panic(err) // the Go tool was tested to work earlier - } - return strings.TrimSuffix(string(val), "\n") -} - func TestBuild(t *testing.T) { const listenAddr = "127.0.0.1:40573" var err error - cmd := exec.Command("go", "run", "_scripts/make.go", "build") - cmd.Dir = projectRoot() - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) - } - - dlvbin := filepath.Join(cmd.Dir, "dlv") + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() buildtestdir := filepath.Join(fixtures, "buildtest") - cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen="+listenAddr, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") + cmd := exec.Command(dlvbin, "debug", "--headless=true", "--listen="+listenAddr, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") cmd.Dir = buildtestdir stderr, err := cmd.StderrPipe() assertNoError(err, t, "stderr pipe") @@ -344,7 +317,7 @@ func TestRedirect(t *testing.T) { const checkAutogenDocLongOutput = false func checkAutogenDoc(t *testing.T, filename, gencommand string, generated []byte) { - saved := slurpFile(t, filepath.Join(projectRoot(), filename)) + saved := slurpFile(t, filepath.Join(protest.ProjectRoot(), filename)) saved = bytes.ReplaceAll(saved, []byte("\r\n"), []byte{'\n'}) generated = bytes.ReplaceAll(generated, []byte("\r\n"), []byte{'\n'}) @@ -382,7 +355,7 @@ func diffMaybe(t *testing.T, filename string, generated []byte) { return } cmd := exec.Command("diff", filename, "-") - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() stdin, _ := cmd.StdinPipe() go func() { stdin.Write(generated) @@ -412,7 +385,7 @@ func TestGeneratedDoc(t *testing.T) { // Checks gen-usage-docs.go tempDir := t.TempDir() cmd := exec.Command("go", "run", "_scripts/gen-usage-docs.go", tempDir) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() err := cmd.Run() assertNoError(err, t, "go run _scripts/gen-usage-docs.go") entries, err := os.ReadDir(tempDir) @@ -426,7 +399,7 @@ func TestGeneratedDoc(t *testing.T) { a := []string{"run"} a = append(a, args...) cmd := exec.Command("go", a...) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("could not run script %v: %v (output: %q)", args, err, string(out)) @@ -1383,7 +1356,7 @@ func TestStaticcheck(t *testing.T) { // where we don't do this it is a deliberate style choice. // * ST1023 "Redundant type in variable declaration" same as S1021. cmd := exec.Command("staticcheck", args...) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") out, _ := cmd.CombinedOutput() checkAutogenDoc(t, "_scripts/staticcheck-out.txt", fmt.Sprintf("staticcheck %s > _scripts/staticcheck-out.txt", strings.Join(args, " ")), out) @@ -1435,21 +1408,14 @@ func TestUnixDomainSocket(t *testing.T) { var err error - cmd := exec.Command("go", "run", "_scripts/make.go", "build") - cmd.Dir = projectRoot() - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) - } - - dlvbin := filepath.Join(cmd.Dir, "dlv") + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() buildtestdir := filepath.Join(fixtures, "buildtest") - cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen=unix:"+listenPath, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") + cmd := exec.Command(dlvbin, "debug", "--headless=true", "--listen=unix:"+listenPath, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") cmd.Dir = buildtestdir stderr, err := cmd.StderrPipe() assertNoError(err, t, "stderr pipe") diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index d861bd59b5..563b6862c6 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -652,6 +652,13 @@ func (fn *Function) privateRuntime() bool { return len(name) > n && name[:n] == "runtime." && !('A' <= name[n] && name[n] <= 'Z') } +func (fn *Function) CompilationUnitName() string { + if fn.cu == nil { + return "" + } + return fn.cu.name +} + func rangeParentName(fnname string) int { const rangeSuffix = "-range" ridx := strings.Index(fnname, rangeSuffix) diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go index f1d2ae64a5..331b802a8a 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -389,3 +389,37 @@ func RegabiSupported() bool { return false } } + +func ProjectRoot() string { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + gopaths := strings.FieldsFunc(os.Getenv("GOPATH"), func(r rune) bool { return r == os.PathListSeparator }) + for _, curpath := range gopaths { + // Detects "gopath mode" when GOPATH contains several paths ex. "d:\\dir\\gopath;f:\\dir\\gopath2" + if strings.Contains(wd, curpath) { + return filepath.Join(curpath, "src", "github.com", "go-delve", "delve") + } + } + val, err := exec.Command("go", "list", "-mod=", "-m", "-f", "{{ .Dir }}").Output() + if err != nil { + panic(err) // the Go tool was tested to work earlier + } + return strings.TrimSuffix(string(val), "\n") +} + +func GetDlvBinary(t *testing.T) string { + cmd := exec.Command("go", "run", "_scripts/make.go", "build") + cmd.Dir = ProjectRoot() + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) + } + + if runtime.GOOS == "windows" { + return filepath.Join(cmd.Dir, "dlv.exe") + } + return filepath.Join(cmd.Dir, "dlv") +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index d90e465737..87b90e5c10 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -555,10 +555,13 @@ Changes the value of a configuration parameter. config substitute-path config substitute-path config substitute-path -clear + config substitute-path -guess Adds or removes a path substitution rule, if -clear is used all substitute-path rules are removed. Without arguments shows the current list of substitute-path rules. +The -guess option causes Delve to try to guess your substitute-path +configuration automatically. See also Documentation/cli/substitutepath.md for how the rules are applied. config alias diff --git a/pkg/terminal/config.go b/pkg/terminal/config.go index 096014da3f..1405bac64f 100644 --- a/pkg/terminal/config.go +++ b/pkg/terminal/config.go @@ -73,6 +73,17 @@ func configureSetSubstitutePath(t *Term, rest string) error { t.conf.SubstitutePath = t.conf.SubstitutePath[:0] return nil } + if strings.TrimSpace(rest) == "-guess" { + rules, err := t.client.GuessSubstitutePath() + if err != nil { + return err + } + t.conf.SubstitutePath = t.conf.SubstitutePath[:0] + for _, rule := range rules { + t.conf.SubstitutePath = append(t.conf.SubstitutePath, config.SubstitutePathRule{From: rule[0], To: rule[1]}) + } + rest = "" // print the result + } argv := config.SplitQuotedFields(rest, '"') if len(argv) == 2 && argv[0] == "-clear" { argv = argv[1:] diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 3add7e377f..ccbcdf65c4 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -956,6 +956,37 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) { return env.interfaceToStarlarkValue(rpcRet), nil }) doc["get_thread"] = "builtin get_thread(Id)\n\nget_thread gets a thread by its ID." + r["guess_substitute_path"] = starlark.NewBuiltin("guess_substitute_path", 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.GuessSubstitutePathIn + var rpcRet rpc2.GuessSubstitutePathOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.Args, "Args") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "Args": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Args, "Args") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("GuessSubstitutePath", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) + doc["guess_substitute_path"] = "builtin guess_substitute_path(Args)" r["is_multiclient"] = starlark.NewBuiltin("is_multiclient", 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 3073892b3c..2ddde601ec 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -677,3 +677,10 @@ type Target struct { CmdLine string CurrentThread *Thread } + +// GuessSubstitutePathIn are the input parameters used to guess a substitute-path configuration automatically. +type GuessSubstitutePathIn struct { + ImportPathOfMainPackage string + ClientGOROOT string + ClientModuleDirectories map[string]string +} diff --git a/service/client.go b/service/client.go index baf881ef97..b667e6ae72 100644 --- a/service/client.go +++ b/service/client.go @@ -205,6 +205,9 @@ type Client interface { // GetDebugInfoDirectories returns the list of directories used to search for debug symbols GetDebugInfoDirectories() ([]string, error) + // GuessSubstitutePath tries to guess a substitute-path configuration for the client + GuessSubstitutePath() ([][2]string, error) + // CallAPI allows calling an arbitrary rpc method (used by starlark bindings) CallAPI(method string, args, reply interface{}) error } diff --git a/service/dap/server.go b/service/dap/server.go index 5ba6f0f292..b85b5205db 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1975,6 +1975,18 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) { s.setLaunchAttachArgs(args.LaunchAttachCommonConfig) + if len(args.LaunchAttachCommonConfig.SubstitutePath) == 0 && args.GuessSubstitutePath != nil && s.debugger != nil { + server2Client := s.debugger.GuessSubstitutePath(args.GuessSubstitutePath) + clientToServer := make([][2]string, 0, len(server2Client)) + serverToClient := make([][2]string, 0, len(server2Client)) + for serverDir, clientDir := range server2Client { + serverToClient = append(serverToClient, [2]string{serverDir, clientDir}) + clientToServer = append(clientToServer, [2]string{clientDir, serverDir}) + } + s.args.substitutePathClientToServer = clientToServer + s.args.substitutePathServerToClient = serverToClient + } + // Notify the client that the debugger is ready to start accepting // configuration requests for setting breakpoints, etc. The client // will end the configuration sequence with 'configurationDone'. diff --git a/service/dap/types.go b/service/dap/types.go index 59f06065ab..3a76c28ae3 100644 --- a/service/dap/types.go +++ b/service/dap/types.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + + "github.com/go-delve/delve/service/api" ) // Launch debug sessions support the following modes: @@ -252,6 +254,11 @@ type AttachConfig struct { // Wait for a process with a name beginning with this prefix. AttachWaitFor string `json:"waitFor,omitempty"` + // GuessSubstitutePath is used to automatically guess SubstitutePath if it + // is not specified explicitly. It should be copied from the output of + // 'dlv substitute-path-guess-helper'. + GuessSubstitutePath *api.GuessSubstitutePathIn + LaunchAttachCommonConfig } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index a638793662..84d1447950 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -12,6 +12,7 @@ import ( "io" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" @@ -2565,3 +2566,157 @@ func (d *Debugger) maybePrintUnattendedBreakpointWarning(stopReason proc.StopRea } api.PrintStack(formatPathFunc, os.Stderr, apiFrames, "", false, api.StackTraceColors{}, includeFunc) } + +// GuessSubstitutePath returns a substitute-path configuration that maps +// server paths to client paths by examining the executable file and a map +// of module paths to client directories (clientMod2Dir) passed as input. +func (d *Debugger) GuessSubstitutePath(args *api.GuessSubstitutePathIn) map[string]string { + bis := []*proc.BinaryInfo{} + bins := [][]proc.Function{} + tgt := proc.ValidTargets{Group: d.target} + for tgt.Next() { + bi := tgt.BinInfo() + bis = append(bis, bi) + bins = append(bins, bi.Functions) + } + return guessSubstitutePath(args, bins, func(biIdx int, fn *proc.Function) string { + file, _ := bis[biIdx].EntryLineForFunc(fn) + return file + }) +} + +func guessSubstitutePath(args *api.GuessSubstitutePathIn, bins [][]proc.Function, fileForFunc func(int, *proc.Function) string) map[string]string { + serverMod2Dir := map[string]string{} + serverMod2DirCandidate := map[string]map[string]int{} + pkg2mod := map[string]string{} + + for mod := range args.ClientModuleDirectories { + serverMod2DirCandidate[mod] = make(map[string]int) + } + + const minEvidence = 10 + const decisionThreshold = 0.8 + + totCandidates := func(mod string) int { + r := 0 + for _, cnt := range serverMod2DirCandidate[mod] { + r += cnt + } + return r + } + + bestCandidate := func(mod string) string { + best := "" + for dir, cnt := range serverMod2DirCandidate[mod] { + if cnt > serverMod2DirCandidate[mod][best] { + best = dir + } + } + return best + } + + slashes := func(s string) int { + r := 0 + for _, ch := range s { + if ch == '/' { + r++ + } + } + return r + } + + serverGoroot := "" + + logger := logflags.DebuggerLogger() + + for binIdx, bin := range bins { + for i := range bin { + fn := &bin[i] + + if fn.Name == "runtime.main" && serverGoroot == "" { + file := fileForFunc(binIdx, fn) + serverGoroot = path.Dir(path.Dir(path.Dir(file))) + continue + } + + fnpkg := fn.PackageName() + if fn.CompilationUnitName() != "" && strings.ReplaceAll(fn.CompilationUnitName(), "\\", "/") != fnpkg { + // inlined + continue + } + + if fnpkg == "main" && binIdx == 0 && args.ImportPathOfMainPackage != "" { + fnpkg = args.ImportPathOfMainPackage + } + + fnmod := "" + + if mod, ok := pkg2mod[fnpkg]; ok { + fnmod = mod + } else { + for mod := range args.ClientModuleDirectories { + if strings.HasPrefix(fnpkg, mod) { + fnmod = mod + break + } + } + pkg2mod[fnpkg] = fnmod + if fnmod == "" { + logger.Debugf("No module detected for server package %q", fnpkg) + } + } + + if fnmod == "" { + // not in any module we are interested in + continue + } + if serverMod2Dir[fnmod] != "" { + // already decided + continue + } + + elems := slashes(fnpkg[len(fnmod):]) + + file := fileForFunc(binIdx, fn) + if file == "" || file == "" { + continue + } + logger.Debugf("considering %s pkg:%s compile unit:%s file:%s", fn.Name, fnpkg, fn.CompilationUnitName(), file) + dir := path.Dir(file) // note: paths are normalized to always use '/' as a separator by pkg/dwarf/line + if slashes(dir) < elems { + continue + } + for i := 0; i < elems; i++ { + dir = path.Dir(dir) + } + + serverMod2DirCandidate[fnmod][dir]++ + + n := totCandidates(fnmod) + best := bestCandidate(fnmod) + if n > minEvidence && float64(serverMod2DirCandidate[fnmod][best])/float64(n) > decisionThreshold { + serverMod2Dir[fnmod] = best + } + } + } + + for mod := range args.ClientModuleDirectories { + if serverMod2Dir[mod] == "" { + serverMod2Dir[mod] = bestCandidate(mod) + } + } + + server2Client := make(map[string]string) + + for mod, clientDir := range args.ClientModuleDirectories { + if serverMod2Dir[mod] != "" { + server2Client[serverMod2Dir[mod]] = clientDir + } + } + + if serverGoroot != "" && args.ClientGOROOT != "" { + server2Client[serverGoroot] = args.ClientGOROOT + } + + return server2Client +} diff --git a/service/debugger/debugger_test.go b/service/debugger/debugger_test.go index c493ff9804..cb364cef43 100644 --- a/service/debugger/debugger_test.go +++ b/service/debugger/debugger_test.go @@ -1,6 +1,7 @@ package debugger import ( + "flag" "fmt" "os" "path/filepath" @@ -9,10 +10,20 @@ import ( "testing" "github.com/go-delve/delve/pkg/gobuild" + "github.com/go-delve/delve/pkg/logflags" + "github.com/go-delve/delve/pkg/proc" protest "github.com/go-delve/delve/pkg/proc/test" "github.com/go-delve/delve/service/api" ) +func TestMain(m *testing.M) { + var logConf string + flag.StringVar(&logConf, "log", "", "configures logging") + flag.Parse() + logflags.Setup(logConf != "", logConf, "") + os.Exit(protest.RunTestsWithFixtures(m)) +} + func TestDebugger_LaunchNoMain(t *testing.T) { fixturesDir := protest.FindFixturesDir() nomaindir := filepath.Join(fixturesDir, "nomaindir") @@ -97,3 +108,45 @@ func TestDebugger_LaunchCurrentDir(t *testing.T) { t.Fatal(err) } } + +func guessSubstitutePathHelper(t *testing.T, args *api.GuessSubstitutePathIn, fnpaths [][2]string, tgt map[string]string) { + const base = 0x40000 + t.Helper() + bins := [][]proc.Function{[]proc.Function{}} + for i, fnpath := range fnpaths { + bins[0] = append(bins[0], proc.Function{Name: fnpath[0], Entry: uint64(base + i)}) + } + out := guessSubstitutePath(args, bins, func(_ int, fn *proc.Function) string { + return fnpaths[fn.Entry-base][1] + }) + t.Logf("%#v\n", out) + if len(out) != len(tgt) { + t.Errorf("wrong number of entries") + return + } + for k := range out { + if out[k] != tgt[k] { + t.Errorf("mismatch for directory %q", k) + return + } + } +} + +func TestGuessSubstitutePathMinimalMain(t *testing.T) { + // When the main module only contains a single package check that its mapping still works + guessSubstitutePathHelper(t, + &api.GuessSubstitutePathIn{ + ImportPathOfMainPackage: "github.com/ccampo133/go-docker-alpine-remote-debug", + ClientGOROOT: "/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64", + ClientModuleDirectories: map[string]string{ + "github.com/ccampo133/go-docker-alpine-remote-debug": "/user/gohome/go-docker-alpine-remote-debug", + }, + }, + [][2]string{ + {"main.main", "/app/main.go"}, + {"main.hello", "/app/main.go"}, + {"runtime.main", "/usr/local/go/src/runtime/main.go"}}, + map[string]string{ + "/usr/local/go": "/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64", + "/app": "/user/gohome/go-docker-alpine-remote-debug"}) +} diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 3b0da21ebc..8270d741af 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -1,11 +1,16 @@ package rpc2 import ( + "bytes" + "encoding/json" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" + "os/exec" + "path/filepath" + "strings" "time" "github.com/go-delve/delve/service" @@ -578,6 +583,86 @@ func (c *RPCClient) GetDebugInfoDirectories() ([]string, error) { return out.List, err } +type goListEntry struct { + Dir string + ImportPath string + Name string + Module *goListModule +} + +type goListModule struct { + Path string +} + +// MakeGuessSusbtitutePathIn returns a mapping from modules to client +// directories using "go list". +func MakeGuessSusbtitutePathIn() (*api.GuessSubstitutePathIn, error) { + cmd := exec.Command("go", "list", "--json", "all") + buf, err := cmd.Output() + if err != nil { + return nil, err + } + importPathOfMainPackage := "" + importPathOfMainPackageOk := true + mod2dir := make(map[string]string) + d := json.NewDecoder(bytes.NewReader(buf)) + for d.More() { + var e goListEntry + err := d.Decode(&e) + if err != nil { + return nil, err + } + if e.Module == nil { + continue + } + if !strings.HasPrefix(e.ImportPath, e.Module.Path) { + continue + } + pkgWithoutModule := e.ImportPath[len(e.Module.Path):] + elems := 0 + for _, c := range pkgWithoutModule { + if c == '/' { + elems++ + } + } + dir := e.Dir + for i := 0; i < elems; i++ { + dir = filepath.Dir(dir) + } + if mod2dir[e.Module.Path] != "" && mod2dir[e.Module.Path] != dir { + return nil, fmt.Errorf("could not determine path for module %s (got %q and %q)", e.Module.Path, mod2dir[e.Module.Path], dir) + } + mod2dir[e.Module.Path] = dir + if e.Name == "main" { + if importPathOfMainPackage != "" && importPathOfMainPackage != e.ImportPath { + importPathOfMainPackageOk = false + } + importPathOfMainPackage = e.ImportPath + } + } + buf, err = exec.Command("go", "env", "GOROOT").Output() + if err != nil { + return nil, err + } + clientGoroot := strings.TrimSpace(string(buf)) + if !importPathOfMainPackageOk { + // There were multiple main packages + importPathOfMainPackage = "" + } + return &api.GuessSubstitutePathIn{ClientGOROOT: clientGoroot, ImportPathOfMainPackage: importPathOfMainPackage, ClientModuleDirectories: mod2dir}, nil +} + +func (c *RPCClient) GuessSubstitutePath() ([][2]string, error) { + in, err := MakeGuessSusbtitutePathIn() + if err != nil { + return nil, err + } + + out := &GuessSubstitutePathOut{} + err = c.call("GuessSubstitutePath", GuessSubstitutePathIn{*in}, out) + return out.List, err +} + func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 9c92ccade1..e057f1e6e4 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -1144,3 +1144,19 @@ func (s *RPCServer) DebugInfoDirectories(arg DebugInfoDirectoriesIn, out *DebugI out.List = s.debugger.DebugInfoDirectories() return nil } + +type GuessSubstitutePathIn struct { + Args api.GuessSubstitutePathIn +} + +type GuessSubstitutePathOut struct { + List [][2]string +} + +func (s *RPCServer) GuessSubstitutePath(arg GuessSubstitutePathIn, out *GuessSubstitutePathOut) error { + m := s.debugger.GuessSubstitutePath(&arg.Args) + for k, v := range m { + out.List = append(out.List, [2]string{k, v}) + } + return nil +} diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 6fc5f642d9..7daf1947f1 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -3167,3 +3167,132 @@ func TestBreakpointVariablesWithoutG(t *testing.T) { assertNoError(state.Err, t, "Continue()") }) } + +func TestGuessSubstitutePath(t *testing.T) { + t.Setenv("NOCERT", "1") + slashnorm := func(s string) string { + if runtime.GOOS != "windows" { + return s + } + return strings.ReplaceAll(s, "\\", "/") + } + + guess := func(t *testing.T, goflags string) [][2]string { + oldgoflags := os.Getenv("GOFLAGS") + os.Setenv("GOFLAGS", goflags) + defer os.Setenv("GOFLAGS", oldgoflags) + + dlvbin := protest.GetDlvBinary(t) + defer os.Remove(dlvbin) + + listener, clientConn := service.ListenerPipe() + defer listener.Close() + server := rpccommon.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: []string{dlvbin, "help"}, + Debugger: debugger.Config{ + Backend: testBackend, + CheckGoVersion: true, + BuildFlags: "", // build flags can be an empty string here because the only test that uses it, does not set special flags. + ExecuteKind: debugger.ExecutingExistingFile, + }, + }) + if err := server.Run(); err != nil { + t.Fatal(err) + } + + client := rpc2.NewClientFromConn(clientConn) + defer client.Detach(true) + + if runtime.GOARCH == "ppc64le" { + os.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") + } + + gsp, err := client.GuessSubstitutePath() + assertNoError(err, t, "GuessSubstitutePath") + return gsp + } + + delvePath := protest.ProjectRoot() + var nmods int = -1 + + t.Run("Normal", func(t *testing.T) { + gsp := guess(t, "") + t.Logf("Normal build: %d", len(gsp)) + if len(gsp) == 0 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] != slashnorm(e[1]) { + t.Fatalf("mismatch %q %q", e[0], e[1]) + } + if e[1] == delvePath { + found = true + } + } + nmods = len(gsp) + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + + if os.Getenv("CI") == "true" { + return + } + }) + + t.Run("Modules", func(t *testing.T) { + gsp := guess(t, "-mod=mod") + t.Logf("Modules build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == slashnorm(delvePath) && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) + + t.Run("Trimpath", func(t *testing.T) { + gsp := guess(t, "-trimpath") + t.Logf("Trimpath build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == "github.com/go-delve/delve" && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) + + t.Run("ModulesTrimpath", func(t *testing.T) { + gsp := guess(t, "-trimpath -mod=mod") + t.Logf("Modules+Trimpath build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == "github.com/go-delve/delve" && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) +}