Skip to content

Commit

Permalink
service/dap: add substitutePath configuration
Browse files Browse the repository at this point in the history
Similar to substitute-path configuration in the dlv cli, substitutePath
in dap allows users to specify path mappings that are applied to the
source files in stacktrace and breakpoint requests.

Updates go-delve#2203
  • Loading branch information
suzmue committed Feb 25, 2021
1 parent 2e80b32 commit b0f5780
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 12 deletions.
87 changes: 75 additions & 12 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"

"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"
Expand Down Expand Up @@ -78,13 +79,19 @@ type launchAttachArgs struct {
stackTraceDepth int
// showGlobalVariables indicates if global package variables should be loaded.
showGlobalVariables bool
// substitutePathLocalToDebugger indicates rules for converting file paths between client and debugger.
substitutePathLocalToDebugger [][2]string
// substitutePathLocalToDebugger indicates rules for converting file paths between client and debugger.
substitutePathDebuggerToLocal [][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,
substitutePathLocalToDebugger: [][2]string{},
substitutePathDebuggerToLocal: [][2]string{},
}

// DefaultLoadConfig controls how variables are loaded from the target's memory, borrowing the
Expand Down Expand Up @@ -119,7 +126,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
Expand All @@ -132,6 +139,34 @@ func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) {
if ok {
s.args.showGlobalVariables = globals
}
rules, ok := request.GetArguments()["substitutePath"]
if ok {
rulesParsed, ok := rules.([]interface{})
if !ok {
return fmt.Errorf("'substitutePath' attribute '%v' in debug configuration is not a []interface{}", rules)
}
localToDebug := make([][2]string, 0, len(rulesParsed))
debugToLocal := make([][2]string, 0, len(rulesParsed))
for i, arg := range rulesParsed {
r, ok := arg.(map[string]interface{})
if !ok {
return fmt.Errorf("'substitutePath' attribute array element %d '%v' in debug configuration is not a map[string]interface{}", i, arg)
}
from, ok := r["from"].(string)
if !ok {
return fmt.Errorf("'from' in array element %d for 'substitutePath' '%v' in debug configuration is not a string", i, r["from"])
}
to, ok := r["to"].(string)
if !ok {
return fmt.Errorf("'to' in array element %d for 'substitutePath' '%v' in debug configuration is not a string", i, r["to"])
}
localToDebug = append(localToDebug, [2]string{from, to})
debugToLocal = append(debugToLocal, [2]string{to, from})
}
s.args.substitutePathLocalToDebugger = localToDebug
s.args.substitutePathDebuggerToLocal = debugToLocal
}
return nil
}

// Stop stops the DAP debugger service, closes the listener and the client
Expand Down Expand Up @@ -494,7 +529,13 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
return
}

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"]
Expand All @@ -521,7 +562,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
s.config.ProcessArgs = append([]string{program}, targetArgs...)
s.config.Debugger.WorkingDir = filepath.Dir(program)

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())
Expand Down Expand Up @@ -570,6 +610,9 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
return
}

localPath := request.Arguments.Source.Path
debuggerPath := s.toDebuggerPath(localPath)

// 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.
Expand All @@ -590,7 +633,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 != debuggerPath {
continue
}
_, err := s.debugger.ClearBreakpoint(bp)
Expand All @@ -605,15 +648,15 @@ 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: debuggerPath, Line: want.Line, Cond: want.Condition})
response.Body.Breakpoints[i].Verified = (err == nil)
if err != nil {
response.Body.Breakpoints[i].Line = want.Line
response.Body.Breakpoints[i].Message = err.Error()
} 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: localPath}
}
}
s.send(response)
Expand Down Expand Up @@ -727,8 +770,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())
Expand Down Expand Up @@ -804,7 +852,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 != "<autogenerated>" {
stackFrames[i].Source = dap.Source{Name: filepath.Base(loc.File), Path: loc.File}
localPath := s.toLocalPath((loc.File))
stackFrames[i].Source = dap.Source{Name: filepath.Base(localPath), Path: localPath}
}
stackFrames[i].Column = 0
}
Expand Down Expand Up @@ -1528,3 +1577,17 @@ func (s *Server) doCommand(command string) {
}})
}
}

func (s *Server) toLocalPath(path string) string {
if len(s.args.substitutePathDebuggerToLocal) == 0 {
return path
}
return locspec.SubstitutePath(path, s.args.substitutePathDebuggerToLocal)
}

func (s *Server) toDebuggerPath(path string) string {
if len(s.args.substitutePathLocalToDebugger) == 0 {
return path
}
return locspec.SubstitutePath(path, s.args.substitutePathLocalToDebugger)
}
83 changes: 83 additions & 0 deletions service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,73 @@ 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) {
localDirPath := filepath.Join(string(filepath.Separator), "path", "that", "does", "not", "exist")
localFilePath := filepath.Join(localDirPath, "loopprog.go")
debuggerDirPath := filepath.Dir(fixture.Source)

runDebugSessionWithBPs(t, client, "launch",
// Attach
func() {
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "exec", "program": fixture.Path, "stopOnEntry": false, "substitutePath": []map[string]string{{"from": localDirPath, "to": debuggerDirPath}}})
},
// Set breakpoints
localFilePath, []int{8},
[]onBreakpoint{{
// Stop at line 8
execute: func() {
handleStop(t, client, 1, "main.loop", 8)
},
disconnect: true,
}})
})
}

// 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) {
localDirPath := filepath.Join(string(filepath.Separator), "path", "that", "does", "not", "exist")
localFilePath := filepath.Join(localDirPath, "loopprog.go")
debuggerDirPath := filepath.Dir(fixture.Source)

// 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)
}

runDebugSessionWithBPs(t, client, "attach",
// Attach
func() {
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": cmd.Process.Pid, "stopOnEntry": false, "substitutePath": []map[string]string{{"from": localDirPath, "to": debuggerDirPath}}})
},
// Set breakpoints
localFilePath, []int{8},
[]onBreakpoint{{
// Stop at line 8
execute: func() {
handleStop(t, client, 1, "main.loop", 8)
},
disconnect: true,
}})
})
}

// expectEval is a helper for verifying the values within an EvaluateResponse.
// value - the value of the evaluated expression
// hasRef - true if the evaluated expression should have children and therefore a non-0 variable reference
Expand Down Expand Up @@ -2546,6 +2613,22 @@ 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 []interface{}")

client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{123}})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: 'substitutePath' attribute array element 0 '123' in debug configuration is not a map[string]interface{}")

client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{map[string]interface{}{"to": "path2"}}})
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: 'from' in array element 0 for 'substitutePath' '<nil>' in debug configuration is not a 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: 'to' in array element 0 for 'substitutePath' '123' in debug configuration is not a string")

// Skip detailed message checks for potentially different OS-specific errors.
client.LaunchRequest("exec", fixture.Path+"_does_not_exist", stopOnEntry)
expectFailedToLaunch(client.ExpectErrorResponse(t))
Expand Down

0 comments on commit b0f5780

Please sign in to comment.