service/dap: add substitutePath configuration (#2379)
* service/dap: add substitutePath configuration 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 #2203 * service/dap: refactor the startup of the fixture for attach Add a helper function for starting up a process to attach to. * service/dap: update substitute path tests for windows * service/dap: remove lines that should have been removed in merge * respond to comments on pr * move logging to helper functions * make test comments more clear * Add comments about absolute paths * fix log messages * clarify test comments * remove comment about absolute paths
This commit is contained in:
parent
747f037883
commit
4eb54b01e7
@ -25,6 +25,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-delve/delve/pkg/gobuild"
|
"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/logflags"
|
||||||
"github.com/go-delve/delve/pkg/proc"
|
"github.com/go-delve/delve/pkg/proc"
|
||||||
"github.com/go-delve/delve/service"
|
"github.com/go-delve/delve/service"
|
||||||
@ -85,13 +86,21 @@ type launchAttachArgs struct {
|
|||||||
stackTraceDepth int
|
stackTraceDepth int
|
||||||
// showGlobalVariables indicates if global package variables should be loaded.
|
// showGlobalVariables indicates if global package variables should be loaded.
|
||||||
showGlobalVariables bool
|
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.
|
// defaultArgs borrows the defaults for the arguments from the original vscode-go adapter.
|
||||||
var defaultArgs = launchAttachArgs{
|
var defaultArgs = launchAttachArgs{
|
||||||
stopOnEntry: false,
|
stopOnEntry: false,
|
||||||
stackTraceDepth: 50,
|
stackTraceDepth: 50,
|
||||||
showGlobalVariables: false,
|
showGlobalVariables: false,
|
||||||
|
substitutePathClientToServer: [][2]string{},
|
||||||
|
substitutePathServerToClient: [][2]string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLoadConfig controls how variables are loaded from the target's memory, borrowing the
|
// 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,
|
// If user-specified options are provided via Launch/AttachRequest,
|
||||||
// we override the defaults for optional args.
|
// 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)
|
stop, ok := request.GetArguments()["stopOnEntry"].(bool)
|
||||||
if ok {
|
if ok {
|
||||||
s.args.stopOnEntry = stop
|
s.args.stopOnEntry = stop
|
||||||
@ -139,6 +148,35 @@ func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) {
|
|||||||
if ok {
|
if ok {
|
||||||
s.args.showGlobalVariables = globals
|
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
|
// 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.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
|
var targetArgs []string
|
||||||
args, ok := request.Arguments["args"]
|
args, ok := request.Arguments["args"]
|
||||||
@ -601,7 +645,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
if s.debugger, err = debugger.New(&s.config.Debugger, s.config.ProcessArgs); err != nil {
|
if s.debugger, err = debugger.New(&s.config.Debugger, s.config.ProcessArgs); err != nil {
|
||||||
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
|
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
|
||||||
return
|
return
|
||||||
@ -724,6 +767,9 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientPath := request.Arguments.Source.Path
|
||||||
|
serverPath := s.toServerPath(clientPath)
|
||||||
|
|
||||||
// According to the spec we should "set multiple breakpoints for a single source
|
// 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
|
// and clear all previous breakpoints in that source." The simplest way is
|
||||||
// to clear all and then set all.
|
// to clear all and then set all.
|
||||||
@ -744,7 +790,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
|
|||||||
}
|
}
|
||||||
// Skip other source files.
|
// Skip other source files.
|
||||||
// TODO(polina): should this be normalized because of different OSes?
|
// TODO(polina): should this be normalized because of different OSes?
|
||||||
if bp.File != request.Arguments.Source.Path {
|
if bp.File != serverPath {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err := s.debugger.ClearBreakpoint(bp)
|
_, 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))
|
response.Body.Breakpoints = make([]dap.Breakpoint, len(request.Arguments.Breakpoints))
|
||||||
for i, want := range request.Arguments.Breakpoints {
|
for i, want := range request.Arguments.Breakpoints {
|
||||||
got, err := s.debugger.CreateBreakpoint(
|
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)
|
response.Body.Breakpoints[i].Verified = (err == nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Body.Breakpoints[i].Line = want.Line
|
response.Body.Breakpoints[i].Line = want.Line
|
||||||
@ -767,7 +813,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
|
|||||||
} else {
|
} else {
|
||||||
response.Body.Breakpoints[i].Id = got.ID
|
response.Body.Breakpoints[i].Id = got.ID
|
||||||
response.Body.Breakpoints[i].Line = got.Line
|
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)
|
s.send(response)
|
||||||
@ -881,8 +927,13 @@ func (s *Server) onAttachRequest(request *dap.AttachRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.config.Debugger.AttachPid = int(pid)
|
s.config.Debugger.AttachPid = int(pid)
|
||||||
s.setLaunchAttachArgs(request)
|
err := s.setLaunchAttachArgs(request)
|
||||||
var err error
|
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 {
|
if s.debugger, err = debugger.New(&s.config.Debugger, nil); err != nil {
|
||||||
s.sendErrorResponse(request.Request,
|
s.sendErrorResponse(request.Request,
|
||||||
FailedToAttach, "Failed to attach", err.Error())
|
FailedToAttach, "Failed to attach", err.Error())
|
||||||
@ -981,7 +1032,8 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
|
|||||||
uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, i})
|
uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, i})
|
||||||
stackFrames[i] = dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc)}
|
stackFrames[i] = dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc)}
|
||||||
if loc.File != "<autogenerated>" {
|
if loc.File != "<autogenerated>" {
|
||||||
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
// TestWorkingDir executes to a breakpoint and tests that the specified
|
||||||
// working directory is the one used to run the program.
|
// working directory is the one used to run the program.
|
||||||
func TestWorkingDir(t *testing.T) {
|
func TestWorkingDir(t *testing.T) {
|
||||||
@ -2702,13 +2791,7 @@ func TestAttachRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) {
|
runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
// Start the program to attach to
|
// Start the program to attach to
|
||||||
// TODO(polina): do I need to sanity check testBackend and runtime.GOOS?
|
cmd := execFixture(t, fixture)
|
||||||
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",
|
runDebugSessionWithBPs(t, client, "attach",
|
||||||
// Attach
|
// Attach
|
||||||
@ -2934,6 +3017,21 @@ func TestBadLaunchRequests(t *testing.T) {
|
|||||||
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
|
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
|
||||||
"Failed to launch: 'buildFlags' attribute '123' in debug configuration is not a string.")
|
"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})
|
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "wd": 123})
|
||||||
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
|
expectFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
|
||||||
"Failed to launch: 'wd' attribute '123' in debug configuration is not a string.")
|
"Failed to launch: 'wd' attribute '123' in debug configuration is not a string.")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user