diff --git a/service/dap/server.go b/service/dap/server.go index 0d04484b0a29f4186ffbce52adec5a5f468b608b..a77f913f878f92c9ef3827778660666c2022c14e 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -25,6 +25,7 @@ import ( "sync" "github.com/go-delve/delve/pkg/gobuild" + "github.com/go-delve/delve/pkg/locspec" "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/service" @@ -85,13 +86,21 @@ type launchAttachArgs struct { stackTraceDepth int // showGlobalVariables indicates if global package variables should be loaded. showGlobalVariables bool + // substitutePathClientToServer indicates rules for converting file paths between client and debugger. + // These must be directory paths. + substitutePathClientToServer [][2]string + // substitutePathServerToClient indicates rules for converting file paths between debugger and client. + // These must be directory paths. + substitutePathServerToClient [][2]string } // defaultArgs borrows the defaults for the arguments from the original vscode-go adapter. var defaultArgs = launchAttachArgs{ - stopOnEntry: false, - stackTraceDepth: 50, - showGlobalVariables: false, + stopOnEntry: false, + stackTraceDepth: 50, + showGlobalVariables: false, + substitutePathClientToServer: [][2]string{}, + substitutePathServerToClient: [][2]string{}, } // DefaultLoadConfig controls how variables are loaded from the target's memory, borrowing the @@ -126,7 +135,7 @@ func NewServer(config *service.Config) *Server { // If user-specified options are provided via Launch/AttachRequest, // we override the defaults for optional args. -func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) { +func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) error { stop, ok := request.GetArguments()["stopOnEntry"].(bool) if ok { s.args.stopOnEntry = stop @@ -139,6 +148,35 @@ func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) { if ok { s.args.showGlobalVariables = globals } + paths, ok := request.GetArguments()["substitutePath"] + if ok { + typeMismatchError := fmt.Errorf("'substitutePath' attribute '%v' in debug configuration is not a []{'from': string, 'to': string}", paths) + pathsParsed, ok := paths.([]interface{}) + if !ok { + return typeMismatchError + } + clientToServer := make([][2]string, 0, len(pathsParsed)) + serverToClient := make([][2]string, 0, len(pathsParsed)) + for _, arg := range pathsParsed { + pathMapping, ok := arg.(map[string]interface{}) + if !ok { + return typeMismatchError + } + from, ok := pathMapping["from"].(string) + if !ok { + return typeMismatchError + } + to, ok := pathMapping["to"].(string) + if !ok { + return typeMismatchError + } + clientToServer = append(clientToServer, [2]string{from, to}) + serverToClient = append(serverToClient, [2]string{to, from}) + } + s.args.substitutePathClientToServer = clientToServer + s.args.substitutePathServerToClient = serverToClient + } + return nil } // Stop stops the DAP debugger service, closes the listener and the client @@ -534,7 +572,13 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) { s.mu.Unlock() } - s.setLaunchAttachArgs(request) + err := s.setLaunchAttachArgs(request) + if err != nil { + s.sendErrorResponse(request.Request, + FailedToLaunch, "Failed to launch", + err.Error()) + return + } var targetArgs []string args, ok := request.Arguments["args"] @@ -601,7 +645,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) { return } - var err error if s.debugger, err = debugger.New(&s.config.Debugger, s.config.ProcessArgs); err != nil { s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error()) return @@ -724,6 +767,9 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) { return } + clientPath := request.Arguments.Source.Path + serverPath := s.toServerPath(clientPath) + // According to the spec we should "set multiple breakpoints for a single source // and clear all previous breakpoints in that source." The simplest way is // to clear all and then set all. @@ -744,7 +790,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) { } // Skip other source files. // TODO(polina): should this be normalized because of different OSes? - if bp.File != request.Arguments.Source.Path { + if bp.File != serverPath { continue } _, err := s.debugger.ClearBreakpoint(bp) @@ -759,7 +805,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) { response.Body.Breakpoints = make([]dap.Breakpoint, len(request.Arguments.Breakpoints)) for i, want := range request.Arguments.Breakpoints { got, err := s.debugger.CreateBreakpoint( - &api.Breakpoint{File: request.Arguments.Source.Path, Line: want.Line, Cond: want.Condition}) + &api.Breakpoint{File: serverPath, Line: want.Line, Cond: want.Condition}) response.Body.Breakpoints[i].Verified = (err == nil) if err != nil { response.Body.Breakpoints[i].Line = want.Line @@ -767,7 +813,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) { } else { response.Body.Breakpoints[i].Id = got.ID response.Body.Breakpoints[i].Line = got.Line - response.Body.Breakpoints[i].Source = dap.Source{Name: request.Arguments.Source.Name, Path: request.Arguments.Source.Path} + response.Body.Breakpoints[i].Source = dap.Source{Name: request.Arguments.Source.Name, Path: clientPath} } } s.send(response) @@ -881,8 +927,13 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) { return } s.config.Debugger.AttachPid = int(pid) - s.setLaunchAttachArgs(request) - var err error + err := s.setLaunchAttachArgs(request) + if err != nil { + s.sendErrorResponse(request.Request, + FailedToAttach, "Failed to attach", + err.Error()) + return + } if s.debugger, err = debugger.New(&s.config.Debugger, nil); err != nil { s.sendErrorResponse(request.Request, FailedToAttach, "Failed to attach", err.Error()) @@ -981,7 +1032,8 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) { uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, i}) stackFrames[i] = dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc)} if loc.File != "" { - stackFrames[i].Source = dap.Source{Name: filepath.Base(loc.File), Path: loc.File} + clientPath := s.toClientPath(loc.File) + stackFrames[i].Source = dap.Source{Name: filepath.Base(clientPath), Path: clientPath} } stackFrames[i].Column = 0 } @@ -1636,3 +1688,25 @@ func (s *Server) doCommand(command string) { }}) } } + +func (s *Server) toClientPath(path string) string { + if len(s.args.substitutePathServerToClient) == 0 { + return path + } + clientPath := locspec.SubstitutePath(path, s.args.substitutePathServerToClient) + if clientPath != path { + s.log.Debugf("server path=%s converted to client path=%s\n", path, clientPath) + } + return clientPath +} + +func (s *Server) toServerPath(path string) string { + if len(s.args.substitutePathClientToServer) == 0 { + return path + } + serverPath := locspec.SubstitutePath(path, s.args.substitutePathClientToServer) + if serverPath != path { + s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath) + } + return serverPath +} diff --git a/service/dap/server_test.go b/service/dap/server_test.go index e4265dfa2085801f67e485208ab6e439df84758c..3faad3626669bc673b3d9d90008f0332f837224a 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -1703,6 +1703,95 @@ func TestSetBreakpoint(t *testing.T) { }) } +// TestLaunchSubstitutePath sets a breakpoint using a path +// that does not exist and expects the substitutePath attribute +// in the launch configuration to take care of the mapping. +func TestLaunchSubstitutePath(t *testing.T) { + runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) { + substitutePathTestHelper(t, fixture, client, "launch", map[string]interface{}{"mode": "exec", "program": fixture.Path}) + }) +} + +// TestAttachSubstitutePath sets a breakpoint using a path +// that does not exist and expects the substitutePath attribute +// in the launch configuration to take care of the mapping. +func TestAttachSubstitutePath(t *testing.T) { + if runtime.GOOS == "freebsd" { + t.SkipNow() + } + if runtime.GOOS == "windows" { + t.Skip("test skipped on windows, see https://delve.beta.teamcity.com/project/Delve_windows for details") + } + runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) { + cmd := execFixture(t, fixture) + + substitutePathTestHelper(t, fixture, client, "attach", map[string]interface{}{"mode": "local", "processId": cmd.Process.Pid}) + }) +} + +func substitutePathTestHelper(t *testing.T, fixture protest.Fixture, client *daptest.Client, request string, launchAttachConfig map[string]interface{}) { + t.Helper() + nonexistentDir := filepath.Join(string(filepath.Separator), "path", "that", "does", "not", "exist") + if runtime.GOOS == "windows" { + nonexistentDir = "C:" + nonexistentDir + } + + launchAttachConfig["stopOnEntry"] = false + // The rules in 'substitutePath' will be applied as follows: + // - mapping paths from client to server: + // The first rule["from"] to match a prefix of 'path' will be applied: + // strings.Replace(path, rule["from"], rule["to"], 1) + // - mapping paths from server to client: + // The first rule["to"] to match a prefix of 'path' will be applied: + // strings.Replace(path, rule["to"], rule["from"], 1) + launchAttachConfig["substitutePath"] = []map[string]string{ + {"from": nonexistentDir, "to": filepath.Dir(fixture.Source)}, + // Since the path mappings are ordered, when converting from client path to + // server path, this mapping will not apply, because nonexistentDir appears in + // an earlier rule. + {"from": nonexistentDir, "to": "this_is_a_bad_path"}, + // Since the path mappings are ordered, when converting from server path to + // client path, this mapping will not apply, because filepath.Dir(fixture.Source) + // appears in an earlier rule. + {"from": "this_is_a_bad_path", "to": filepath.Dir(fixture.Source)}, + } + + runDebugSessionWithBPs(t, client, request, + func() { + switch request { + case "attach": + client.AttachRequest(launchAttachConfig) + case "launch": + client.LaunchRequestWithArgs(launchAttachConfig) + default: + t.Fatalf("invalid request: %s", request) + } + }, + // Set breakpoints + filepath.Join(nonexistentDir, "loopprog.go"), []int{8}, + []onBreakpoint{{ + + execute: func() { + handleStop(t, client, 1, "main.loop", 8) + }, + disconnect: true, + }}) +} + +// execFixture runs the binary fixture.Path and hooks up stdout and stderr +// to os.Stdout and os.Stderr. +func execFixture(t *testing.T, fixture protest.Fixture) *exec.Cmd { + t.Helper() + // TODO(polina): do I need to sanity check testBackend and runtime.GOOS? + cmd := exec.Command(fixture.Path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + return cmd +} + // TestWorkingDir executes to a breakpoint and tests that the specified // working directory is the one used to run the program. func TestWorkingDir(t *testing.T) { @@ -2702,13 +2791,7 @@ func TestAttachRequest(t *testing.T) { } runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) { // Start the program to attach to - // TODO(polina): do I need to sanity check testBackend and runtime.GOOS? - cmd := exec.Command(fixture.Path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - t.Fatal(err) - } + cmd := execFixture(t, fixture) runDebugSessionWithBPs(t, client, "attach", // Attach @@ -2934,6 +3017,21 @@ func TestBadLaunchRequests(t *testing.T) { expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), "Failed to launch: 'buildFlags' attribute '123' in debug configuration is not a string.") + client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": 123}) + expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), + "Failed to launch: 'substitutePath' attribute '123' in debug configuration is not a []{'from': string, 'to': string}") + + client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{123}}) + expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), + "Failed to launch: 'substitutePath' attribute '[123]' in debug configuration is not a []{'from': string, 'to': string}") + + client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{map[string]interface{}{"to": "path2"}}}) + expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), + "Failed to launch: 'substitutePath' attribute '[map[to:path2]]' in debug configuration is not a []{'from': string, 'to': string}") + + client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{map[string]interface{}{"from": "path1", "to": 123}}}) + expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), + "Failed to launch: 'substitutePath' attribute '[map[from:path1 to:123]]' in debug configuration is not a []{'from': string, 'to': string}") client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "wd": 123}) expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t), "Failed to launch: 'wd' attribute '123' in debug configuration is not a string.")