未验证 提交 4eb54b01 编写于 作者: S Suzy Mueller 提交者: GitHub

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
上级 747f0378
......@@ -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 != "<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
}
......@@ -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
// 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.")
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册