From 822014b8e8abfd139be34f32ab7af87d9b590b33 Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Thu, 31 Oct 2024 18:19:08 +0100 Subject: [PATCH] service,terminal,cmd/dlv: automatically guessing substitute-path config (#3781) Add command, API calls and launch.json option to automatically guess substitute-path configuration. --- Documentation/cli/README.md | 3 + Documentation/cli/starlark.md | 1 + _scripts/gen-usage-docs.go | 4 +- _scripts/make.go | 5 +- cmd/dlv/cmds/commands.go | 18 +++ cmd/dlv/dlv_test.go | 127 +++++------------- pkg/proc/bininfo.go | 7 + pkg/proc/test/support.go | 65 +++++++++ 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 | 132 ++++++++++++++++++ 20 files changed, 650 insertions(+), 95 deletions(-) diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index e8b49d8e..c9f19263 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 edb878dd..006a0064 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/_scripts/gen-usage-docs.go b/_scripts/gen-usage-docs.go index 668c90c7..17e5e3b9 100644 --- a/_scripts/gen-usage-docs.go +++ b/_scripts/gen-usage-docs.go @@ -24,7 +24,9 @@ func main() { cmdnames := []string{} for _, subcmd := range root.Commands() { - cmdnames = append(cmdnames, subcmd.Name()) + if !subcmd.Hidden { + cmdnames = append(cmdnames, subcmd.Name()) + } } helphelpers.Prepare(root) doc.GenMarkdownTree(root, usageDir) diff --git a/_scripts/make.go b/_scripts/make.go index 41ee49c5..c681a598 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 @@ -326,7 +329,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 8a3d0313..b5b7662c 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -513,6 +513,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 2a9ad604..57f37a46 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -31,11 +31,6 @@ import ( ) var testBackend string -var ldFlags string - -func init() { - ldFlags = os.Getenv("CGO_LDFLAGS") -} func TestMain(m *testing.M) { flag.StringVar(&testBackend, "backend", "", "selects backend") @@ -61,30 +56,10 @@ 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" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() @@ -194,46 +169,10 @@ func testOutput(t *testing.T, dlvbin, output string, delveCmds []string) (stdout return } -func getDlvBin(t *testing.T) string { - // In case this was set in the environment - // from getDlvBinEBPF lets clear it here, so - // we can ensure we don't get build errors - // depending on the test ordering. - t.Setenv("CGO_LDFLAGS", ldFlags) - var tags string - if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" { - tags = "-tags=exp.winarm64" - } - if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" { - tags = "-tags=exp.linuxppc64le" - } - if runtime.GOOS == "linux" && runtime.GOARCH == "riscv64" { - tags = "-tags=exp.linuxriscv64" - } - return getDlvBinInternal(t, tags) -} - -func getDlvBinEBPF(t *testing.T) string { - return getDlvBinInternal(t, "-tags", "ebpf") -} - -func getDlvBinInternal(t *testing.T, goflags ...string) string { - dlvbin := filepath.Join(t.TempDir(), "dlv.exe") - args := append([]string{"build", "-o", dlvbin}, goflags...) - args = append(args, "github.com/go-delve/delve/cmd/dlv") - - out, err := exec.Command("go", args...).CombinedOutput() - if err != nil { - t.Fatalf("go build -o %v github.com/go-delve/delve/cmd/dlv: %v\n%s", dlvbin, err, string(out)) - } - - return dlvbin -} - // TestOutput verifies that the debug executable is created in the correct path // and removed after exit. func TestOutput(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) for _, output := range []string{"__debug_bin", "myownname", filepath.Join(t.TempDir(), "absolute.path")} { testOutput(t, dlvbin, output, []string{"exit"}) @@ -252,7 +191,7 @@ func TestUnattendedBreakpoint(t *testing.T) { const listenAddr = "127.0.0.1:40573" fixturePath := filepath.Join(protest.FindFixturesDir(), "panic.go") - cmd := exec.Command(getDlvBin(t), "debug", "--continue", "--headless", "--accept-multiclient", "--listen", listenAddr, fixturePath) + cmd := exec.Command(protest.GetDlvBinary(t), "debug", "--continue", "--headless", "--accept-multiclient", "--listen", listenAddr, fixturePath) stderr, err := cmd.StderrPipe() assertNoError(err, t, "stdout pipe") defer stderr.Close() @@ -279,7 +218,7 @@ func TestUnattendedBreakpoint(t *testing.T) { func TestContinue(t *testing.T) { const listenAddr = "127.0.0.1:40573" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr) @@ -311,7 +250,7 @@ func TestContinue(t *testing.T) { func TestRedirect(t *testing.T) { const listenAddr = "127.0.0.1:40573" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) catfixture := filepath.Join(protest.FindFixturesDir(), "cat.go") cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr, "-r", catfixture, catfixture) @@ -339,7 +278,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'}) @@ -377,7 +316,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) @@ -410,7 +349,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) @@ -424,7 +363,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)) @@ -443,7 +382,7 @@ func TestGeneratedDoc(t *testing.T) { } func TestExitInInit(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") exitInit := filepath.Join(protest.FindFixturesDir(), "exit.init") @@ -643,7 +582,7 @@ func TestTypecheckRPC(t *testing.T) { func TestDAPCmd(t *testing.T) { const listenAddr = "127.0.0.1:40575" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--listen", listenAddr) stdout, err := cmd.StdoutPipe() @@ -706,7 +645,7 @@ func newDAPRemoteClient(t *testing.T, addr string, isDlvAttach bool, isMulti boo func TestRemoteDAPClient(t *testing.T) { const listenAddr = "127.0.0.1:40576" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr) @@ -759,7 +698,7 @@ func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client, expectStatus str func TestRemoteDAPClientMulti(t *testing.T) { const listenAddr = "127.0.0.1:40577" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest") cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr) @@ -826,7 +765,7 @@ func TestRemoteDAPClientMulti(t *testing.T) { func TestRemoteDAPClientAfterContinue(t *testing.T) { const listenAddr = "127.0.0.1:40578" - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) fixture := protest.BuildFixture("loopprog", 0) cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr) @@ -887,7 +826,7 @@ func TestDAPCmdWithClient(t *testing.T) { } defer listener.Close() - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr", listener.Addr().String()) buf := &bytes.Buffer{} @@ -934,7 +873,7 @@ func TestDAPCmdWithUnixClient(t *testing.T) { } defer listener.Close() - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr=unix:"+listener.Addr().String()) buf := &bytes.Buffer{} @@ -968,7 +907,7 @@ func TestDAPCmdWithUnixClient(t *testing.T) { } func TestTrace(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) expected := []byte("> goroutine(1): main.foo(99, 9801)\n>> goroutine(1): main.foo => (9900)\n") @@ -992,7 +931,7 @@ func TestTrace(t *testing.T) { } func TestTrace2(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) expected := []byte("> goroutine(1): main.callme(2)\n>> goroutine(1): main.callme => (4)\n") @@ -1016,7 +955,7 @@ func TestTrace2(t *testing.T) { } func TestTraceDirRecursion(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) expected := []byte("> goroutine(1):frame(1) main.A(5, 5)\n > goroutine(1):frame(2) main.A(4, 4)\n > goroutine(1):frame(3) main.A(3, 3)\n > goroutine(1):frame(4) main.A(2, 2)\n > goroutine(1):frame(5) main.A(1, 1)\n >> goroutine(1):frame(5) main.A => (1)\n >> goroutine(1):frame(4) main.A => (2)\n >> goroutine(1):frame(3) main.A => (6)\n >> goroutine(1):frame(2) main.A => (24)\n>> goroutine(1):frame(1) main.A => (120)\n") @@ -1049,7 +988,7 @@ func TestTraceDirRecursion(t *testing.T) { } func TestTraceMultipleGoroutines(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) // TODO(derekparker) this test has to be a bit vague to avoid flakiness. // I think a future improvement could be to use regexp captures to match the @@ -1088,7 +1027,7 @@ func TestTracePid(t *testing.T) { } } - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) expected := []byte("goroutine(1): main.A()\n>> goroutine(1): main.A => ()\n") @@ -1121,7 +1060,7 @@ func TestTracePid(t *testing.T) { } func TestTraceBreakpointExists(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) fixtures := protest.FindFixturesDir() // We always set breakpoints on some runtime functions at startup, so this would return with @@ -1147,7 +1086,7 @@ func TestTraceBreakpointExists(t *testing.T) { } func TestTracePrintStack(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) fixtures := protest.FindFixturesDir() cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(t.TempDir(), "__debug"), "--stack", "2", filepath.Join(fixtures, "issue573.go"), "foo") @@ -1186,7 +1125,7 @@ func TestTraceEBPF(t *testing.T) { t.Skip("test must be run as root") } - dlvbin := getDlvBinEBPF(t) + dlvbin := protest.GetDlvBinaryEBPF(t) expected := []byte("> (1) main.foo(99, 9801)\n=> \"9900\"") @@ -1225,7 +1164,7 @@ func TestTraceEBPF2(t *testing.T) { t.Skip("test must be run as root") } - dlvbin := getDlvBinEBPF(t) + dlvbin := protest.GetDlvBinaryEBPF(t) expected := []byte(`> (1) main.callme(10) > (1) main.callme(9) @@ -1285,7 +1224,7 @@ func TestTraceEBPF3(t *testing.T) { t.Skip("test must be run as root") } - dlvbin := getDlvBinEBPF(t) + dlvbin := protest.GetDlvBinaryEBPF(t) expected := []byte(`> (1) main.tracedFunction(0) > (1) main.tracedFunction(1) @@ -1333,7 +1272,7 @@ func TestTraceEBPF4(t *testing.T) { t.Skip("test must be run as root") } - dlvbin := getDlvBinEBPF(t) + dlvbin := protest.GetDlvBinaryEBPF(t) expected := []byte(`> (1) main.tracedFunction(0, true, 97) > (1) main.tracedFunction(1, false, 98) @@ -1364,7 +1303,7 @@ func TestTraceEBPF4(t *testing.T) { } func TestDlvTestChdir(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) fixtures := protest.FindFixturesDir() @@ -1396,7 +1335,7 @@ func TestDlvTestChdir(t *testing.T) { } func TestVersion(t *testing.T) { - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) got, err := exec.Command(dlvbin, "version", "-v").CombinedOutput() if err != nil { @@ -1425,7 +1364,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) @@ -1434,7 +1373,7 @@ func TestStaticcheck(t *testing.T) { func TestDefaultBinary(t *testing.T) { // Check that when delve is run twice in the same directory simultaneously // it will pick different default output binary paths. - dlvbin := getDlvBin(t) + dlvbin := protest.GetDlvBinary(t) fixture := filepath.Join(protest.FindFixturesDir(), "testargs.go") startOne := func() (io.WriteCloser, func() error, *bytes.Buffer) { @@ -1475,7 +1414,9 @@ func TestUnixDomainSocket(t *testing.T) { listenPath := filepath.Join(tmpdir, "delve_test") - dlvbin := getDlvBin(t) + var err error + + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index c12351aa..dbf3c861 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -665,6 +665,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 c6261b5b..b0b8823a 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -22,6 +22,12 @@ var EnableRace = flag.Bool("racetarget", false, "Enables race detector on inferi var runningWithFixtures bool +var ldFlags string + +func init() { + ldFlags = os.Getenv("CGO_LDFLAGS") +} + // Fixture is a test binary. type Fixture struct { // Name is the short name of the fixture. @@ -392,3 +398,62 @@ 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 { + // In case this was set in the environment + // from getDlvBinEBPF lets clear it here, so + // we can ensure we don't get build errors + // depending on the test ordering. + t.Setenv("CGO_LDFLAGS", ldFlags) + var tags []string + if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" { + tags = []string{"-tags=exp.winarm64"} + } + if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" { + tags = []string{"-tags=exp.linuxppc64le"} + } + if runtime.GOOS == "linux" && runtime.GOARCH == "riscv64" { + tags = []string{"-tags=exp.linuxriscv64"} + } + return getDlvBinInternal(t, tags...) +} + +func GetDlvBinaryEBPF(t *testing.T) string { + return getDlvBinInternal(t, "-tags", "ebpf") +} + +func getDlvBinInternal(t *testing.T, goflags ...string) string { + dlvbin := filepath.Join(t.TempDir(), "dlv.exe") + args := append([]string{"build", "-o", dlvbin}, goflags...) + args = append(args, "github.com/go-delve/delve/cmd/dlv") + + wd, _ := os.Getwd() + fmt.Printf("at %s %s\n", wd, goflags) + + out, err := exec.Command("go", args...).CombinedOutput() + if err != nil { + t.Fatalf("go build -o %v github.com/go-delve/delve/cmd/dlv: %v\n%s", dlvbin, err, string(out)) + } + + return dlvbin +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 706b60c8..51d39b32 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 096014da..1405bac6 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 3add7e37..ccbcdf65 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 3073892b..2ddde601 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 baf881ef..b667e6ae 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 5ba6f0f2..b85b5205 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 59f06065..1948599d 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 `json:"guessSubstitutePath,omitempty"` + LaunchAttachCommonConfig } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 99476ad4..52299ad8 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" @@ -2551,3 +2552,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 a85ca6b1..e83b4120 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") @@ -100,3 +111,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 3b0da21e..8270d741 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 cfef5f60..4a0307b1 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -1146,3 +1146,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 6fc5f642..04a4e854 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -3167,3 +3167,135 @@ 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) + + switch runtime.GOARCH { + case "ppc64le": + os.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") + case "riscv64": + os.Setenv("GOFLAGS", "-tags=exp.linuxriscv64") + } + + 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) + } + }) +}