dap: define LaunchConfig/AttachConfig types (#2571)

Formally define these types and document their meaning.
We will auto-generate the dlv-dap documentation from these Go type doc.

mapToStruct is a helper that sets the given struct's fields with the
info in map[string]interface{} (launch/attach's Arguments). We achieve
this by reencoding map[string]interface{} to json and decoding back to
the target struct. If go-dap left the implementation-specific arguments
as json.RawMessage and let the implementation decode as needed, this
reencoding could've been avoided.

encoding/json itself does not have mean to enforce required fields.
There was a test case that checks substitutePath elements must set
both from/to fields. Path.UnmarshalJSON implements the check.
I am not yet sure about the need for distinction between missing
'from/to' and empty strings yet. (empty value is useful when dealing with
a binary built with trimpath, right?)

A minor behavior change - previously, if noDebug is not a boolean type,
we ignored the attribute silently. Since we use json decoding, any
mismatched types will cause an error and this non-boolean type noDebug
attribute will result in launch failure.
This commit is contained in:
Hyang-Ah Hana Kim 2021-08-26 08:42:58 -04:00 committed by GitHub
parent 7c91fa0d72
commit 1433c07957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 326 additions and 232 deletions

2
go.mod

@ -7,7 +7,7 @@ require (
github.com/cosiner/argv v0.1.0
github.com/creack/pty v1.1.9
github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9
github.com/google/go-dap v0.5.0
github.com/google/go-dap v0.5.1-0.20210713061233-c91b005e3987
github.com/hashicorp/golang-lru v0.5.4
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.3

6
go.sum

@ -70,8 +70,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-dap v0.5.0 h1:RMHAVn5xeunBakYk65ggHXttk6qjZVdbmi+xhAoL2wY=
github.com/google/go-dap v0.5.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/go-dap v0.5.1-0.20210713061233-c91b005e3987 h1:aghNk+kvabZ5I1OC3cNHWvfZ8svcoDLAGyKYimqyGVk=
github.com/google/go-dap v0.5.1-0.20210713061233-c91b005e3987/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -185,10 +185,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

@ -184,15 +184,20 @@ func (c *Client) InitializeRequestWithArgs(args dap.InitializeRequestArguments)
c.send(request)
}
func toRawMessage(in interface{}) json.RawMessage {
out, _ := json.Marshal(in)
return out
}
// LaunchRequest sends a 'launch' request with the specified args.
func (c *Client) LaunchRequest(mode, program string, stopOnEntry bool) {
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
request.Arguments = map[string]interface{}{
request.Arguments = toRawMessage(map[string]interface{}{
"request": "launch",
"mode": mode,
"program": program,
"stopOnEntry": stopOnEntry,
}
})
c.send(request)
}
@ -201,7 +206,7 @@ func (c *Client) LaunchRequest(mode, program string, stopOnEntry bool) {
// test for values of unexpected types or unspecified values.
func (c *Client) LaunchRequestWithArgs(arguments map[string]interface{}) {
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
request.Arguments = arguments
request.Arguments = toRawMessage(arguments)
c.send(request)
}
@ -209,7 +214,7 @@ func (c *Client) LaunchRequestWithArgs(arguments map[string]interface{}) {
// arguments.
func (c *Client) AttachRequest(arguments map[string]interface{}) {
request := &dap.AttachRequest{Request: *c.newRequest("attach")}
request.Arguments = arguments
request.Arguments = toRawMessage(arguments)
c.send(request)
}

@ -150,6 +150,8 @@ type launchAttachArgs struct {
}
// defaultArgs borrows the defaults for the arguments from the original vscode-go adapter.
// TODO(polinasok): clean up this and its reference (Server.args)
// in favor of default*Config variables defined in types.go.
var defaultArgs = launchAttachArgs{
stopOnEntry: false,
stackTraceDepth: 50,
@ -218,43 +220,18 @@ 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) error {
stop, ok := request.GetArguments()["stopOnEntry"].(bool)
if ok {
s.args.stopOnEntry = stop
func (s *Server) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
s.args.stopOnEntry = args.StopOnEntry
if depth := args.StackTraceDepth; depth > 0 {
s.args.stackTraceDepth = depth
}
depth, ok := request.GetArguments()["stackTraceDepth"].(float64)
if ok && depth > 0 {
s.args.stackTraceDepth = int(depth)
}
globals, ok := request.GetArguments()["showGlobalVariables"].(bool)
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.showGlobalVariables = args.ShowGlobalVariables
if paths := args.SubstitutePath; len(paths) > 0 {
clientToServer := make([][2]string, 0, len(paths))
serverToClient := make([][2]string, 0, len(paths))
for _, p := range paths {
clientToServer = append(clientToServer, [2]string{p.From, p.To})
serverToClient = append(serverToClient, [2]string{p.To, p.From})
}
s.args.substitutePathClientToServer = clientToServer
s.args.substitutePathServerToClient = serverToClient
@ -766,44 +743,38 @@ func cleanExeName(name string) string {
}
func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
// Validate launch request mode
mode, ok := request.Arguments["mode"]
if !ok || mode == "" {
mode = "debug"
}
if !isValidLaunchMode(mode) {
var args = defaultLaunchConfig // narrow copy for initializing non-zero default values
if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("Unsupported 'mode' value %q in debug configuration.", mode))
FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - %v", err))
return
}
mode := args.Mode
if mode == "" {
mode = "debug"
}
if !isValidLaunchMode(mode) {
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", mode))
return
}
// TODO(polina): Respond with an error if debug session is in progress?
program, ok := request.Arguments["program"].(string)
if (!ok || program == "") && mode != "replay" { // Only fail on modes requiring a program
program := args.Program
if program == "" && mode != "replay" { // Only fail on modes requiring a program
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
"The program attribute is missing in debug configuration.")
return
}
backend, ok := request.Arguments["backend"]
if ok {
backendParsed, ok := backend.(string)
if !ok {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("'backend' attribute '%v' in debug configuration is not a string.", backend))
return
}
s.config.Debugger.Backend = backendParsed
if backend := args.Backend; backend != "" {
s.config.Debugger.Backend = backend
} else {
s.config.Debugger.Backend = "default"
}
if mode == "replay" {
traceDirPath, _ := request.Arguments["traceDirPath"].(string)
traceDirPath := args.TraceDirPath
// Validate trace directory
if traceDirPath == "" {
s.sendErrorResponse(request.Request,
@ -818,17 +789,14 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
}
if mode == "core" {
coreFilePath, _ := request.Arguments["coreFilePath"].(string)
coreFilePath := args.CoreFilePath
// Validate core dump path
if coreFilePath == "" {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
"The 'coreFilePath' attribute is missing in debug configuration.")
return
}
// Assign the non-empty core file path to debugger configuration. This will
// trigger a native core file replay instead of an rr trace replay
s.config.Debugger.CoreFile = coreFilePath
@ -839,8 +807,8 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
// Prepare the debug executable filename, build flags and build it
if mode == "debug" || mode == "test" {
output, ok := request.Arguments["output"].(string)
if !ok || output == "" {
output := args.Output
if output == "" {
output = defaultDebugBinary
}
output = cleanExeName(output)
@ -850,17 +818,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
return
}
buildFlags := ""
buildFlagsArg, ok := request.Arguments["buildFlags"]
if ok {
buildFlags, ok = buildFlagsArg.(string)
if !ok {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("'buildFlags' attribute '%v' in debug configuration is not a string.", buildFlagsArg))
return
}
}
buildFlags := args.BuildFlags
var cmd string
var out []byte
@ -890,56 +848,21 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
s.mu.Unlock()
}
err := s.setLaunchAttachArgs(request)
if err != nil {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
err.Error())
if err := s.setLaunchAttachArgs(args.LaunchAttachCommonConfig); err != nil {
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
return
}
var targetArgs []string
args, ok := request.Arguments["args"]
if ok {
argsParsed, ok := args.([]interface{})
if !ok {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("'args' attribute '%v' in debug configuration is not an array.", args))
return
}
for _, arg := range argsParsed {
argParsed, ok := arg.(string)
if !ok {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("value '%v' in 'args' attribute in debug configuration is not a string.", arg))
return
}
targetArgs = append(targetArgs, argParsed)
}
}
s.config.ProcessArgs = append([]string{program}, targetArgs...)
s.config.ProcessArgs = append([]string{program}, args.Args...)
s.config.Debugger.WorkingDir = filepath.Dir(program)
// Set the WorkingDir for this program to the one specified in the request arguments.
wd, ok := request.Arguments["cwd"]
if ok {
wdParsed, ok := wd.(string)
if !ok {
s.sendErrorResponse(request.Request,
FailedToLaunch, "Failed to launch",
fmt.Sprintf("'cwd' attribute '%v' in debug configuration is not a string.", wd))
return
}
s.config.Debugger.WorkingDir = wdParsed
if args.Cwd != "" {
s.config.Debugger.WorkingDir = args.Cwd
}
s.log.Debugf("running binary '%s' in '%s'", program, s.config.Debugger.WorkingDir)
if noDebug, ok := request.Arguments["noDebug"].(bool); ok && noDebug {
if args.NoDebug {
s.mu.Lock()
cmd, err := s.newNoDebugProcess(program, targetArgs, s.config.Debugger.WorkingDir)
cmd, err := s.newNoDebugProcess(program, args.Args, s.config.Debugger.WorkingDir)
s.mu.Unlock()
if err != nil {
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
@ -968,6 +891,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
return
}
var err error
func() {
s.mu.Lock()
defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error
@ -1021,39 +945,6 @@ func (s *Server) stopNoDebugProcess() {
s.noDebugProcess = nil
}
// Launch debug sessions support the following modes:
// -- [DEFAULT] "debug" - builds and launches debugger for specified program (similar to 'dlv debug')
// Required args: program
// Optional args with default: output, cwd, noDebug
// Optional args: buildFlags, args
// -- "test" - builds and launches debugger for specified test (similar to 'dlv test')
// same args as above
// -- "exec" - launches debugger for precompiled binary (similar to 'dlv exec')
// Required args: program
// Optional args with default: cwd, noDebug
// Optional args: args
// -- replay: skips program build and sets the Debugger.CoreFile property based on the
// Required args: coreFilePath
// Optional args: program, args
// -- core: skips program build and sets the Debugger.CoreFile property based on the
// Required args: program, traceDirPath
// Optional args: args
func isValidLaunchMode(mode interface{}) bool {
switch mode {
case "exec", "debug", "test", "replay", "core":
return true
}
return false
}
// Attach debug sessions support the following modes:
// -- [DEFAULT] "local" -- attaches debugger to a local running process
// Required args: processId
// TODO(polina): support "remote" mode
func isValidAttachMode(mode interface{}) bool {
return mode == "local"
}
// onDisconnectRequest handles the DisconnectRequest. Per the DAP spec,
// it disconnects the debuggee and signals that the debug adaptor
// (in our case this TCP server) can be terminated.
@ -1524,46 +1415,40 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
// onAttachRequest handles 'attach' request.
// This is a mandatory request to support.
func (s *Server) onAttachRequest(request *dap.AttachRequest) {
mode, ok := request.Arguments["mode"]
if !ok || mode == "" {
mode = "local"
}
if !isValidAttachMode(mode) {
// TODO(polina): support 'remote' mode that expects a non-nil debugger
s.sendErrorResponse(request.Request,
FailedToAttach, "Failed to attach",
fmt.Sprintf("Unsupported 'mode' value %q in debug configuration", mode))
var args AttachConfig = defaultAttachConfig // narrow copy for initializing non-zero default values
if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
s.sendErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - %v", err))
return
}
mode := args.Mode
if mode == "" {
mode = "local"
}
if !isValidAttachMode(mode) {
s.sendErrorResponse(request.Request, FailedToAttach, "Failed to attach",
fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", args.Mode))
return
}
if mode == "local" {
pid, ok := request.Arguments["processId"].(float64)
if !ok || pid == 0 {
if args.ProcessID == 0 {
s.sendErrorResponse(request.Request,
FailedToAttach, "Failed to attach",
"The 'processId' attribute is missing in debug configuration")
return
}
s.config.Debugger.AttachPid = int(pid)
err := s.setLaunchAttachArgs(request)
if err != nil {
s.config.Debugger.AttachPid = args.ProcessID
if err := s.setLaunchAttachArgs(args.LaunchAttachCommonConfig); err != nil {
s.sendErrorResponse(request.Request, FailedToAttach, "Failed to attach", err.Error())
return
}
backend, ok := request.Arguments["backend"]
if ok {
backendParsed, ok := backend.(string)
if !ok {
s.sendErrorResponse(request.Request,
FailedToAttach, "Failed to attach",
fmt.Sprintf("'backend' attribute '%v' in debug configuration is not a string.", backend))
return
}
s.config.Debugger.Backend = backendParsed
if backend := args.Backend; backend != "" {
s.config.Debugger.Backend = backend
} else {
s.config.Debugger.Backend = "default"
}
var err error
func() {
s.mu.Lock()
defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error

@ -4117,14 +4117,6 @@ func TestLaunchRequestDefaults(t *testing.T) {
// writes to default output dir __debug_bin
})
})
// if noDebug is not a bool, behave as if it is the default value (false).
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSession(t, client, "launch", func() {
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "debug", "program": fixture.Source, "noDebug": "true"})
})
})
}
// TestLaunchRequestOutputPath verifies that relative output binary path
@ -4428,10 +4420,6 @@ type helperForSetVariable struct {
c *daptest.Client
}
func (h *helperForSetVariable) expectSetVariableAndStop(ref int, name, value string) {
h.t.Helper()
h.expectSetVariable0(ref, name, value, true)
}
func (h *helperForSetVariable) expectSetVariable(ref int, name, value string) {
h.t.Helper()
h.expectSetVariable0(ref, name, value, false)
@ -4867,7 +4855,7 @@ func TestBadLaunchRequests(t *testing.T) {
// Bad "program"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": 12345})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: The program attribute is missing in debug configuration.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"program\" of type string")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": nil})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
@ -4880,15 +4868,15 @@ func TestBadLaunchRequests(t *testing.T) {
// Bad "mode"
client.LaunchRequest("remote", fixture.Path, stopOnEntry)
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: Unsupported 'mode' value \"remote\" in debug configuration.")
"Failed to launch: invalid debug configuration - unsupported 'mode' attribute \"remote\"")
client.LaunchRequest("notamode", fixture.Path, stopOnEntry)
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: Unsupported 'mode' value \"notamode\" in debug configuration.")
"Failed to launch: invalid debug configuration - unsupported 'mode' attribute \"notamode\"")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": 12345, "program": fixture.Path})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: Unsupported 'mode' value %!q(float64=12345) in debug configuration.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"mode\" of type string")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": ""}) // empty mode defaults to "debug" (not an error)
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
@ -4899,55 +4887,51 @@ func TestBadLaunchRequests(t *testing.T) {
"Failed to launch: The program attribute is missing in debug configuration.")
// Bad "args"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "exec", "program": fixture.Path, "args": nil})
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "exec", "program": fixture.Path, "args": "foobar"})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'args' attribute '<nil>' in debug configuration is not an array.")
"Failed to launch: invalid debug configuration - cannot unmarshal string into \"args\" of type []string")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "exec", "program": fixture.Path, "args": 12345})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'args' attribute '12345' in debug configuration is not an array.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"args\" of type []string")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "exec", "program": fixture.Path, "args": []int{1, 2}})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: value '1' in 'args' attribute in debug configuration is not a string.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"args\" of type string")
// Bad "buildFlags"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "buildFlags": 123})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'buildFlags' attribute '123' in debug configuration is not a string.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"buildFlags\" of type string")
// Bad "backend"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "backend": 123})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'backend' attribute '123' in debug configuration is not a string.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"backend\" of type string")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "backend": "foo"})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: could not launch process: unknown backend \"foo\"")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "backend": ""})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: could not launch process: unknown backend \"\"")
// Bad "substitutePath"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": 123})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'substitutePath' attribute '123' in debug configuration is not a []{'from': string, 'to': string}")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"substitutePath\" of type {\"from\":string, \"to\":string}")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{123}})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'substitutePath' attribute '[123]' in debug configuration is not a []{'from': string, 'to': string}")
"Failed to launch: invalid debug configuration - cannot use 123 as 'substitutePath' of type {\"from\":string, \"to\":string}")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{map[string]interface{}{"to": "path2"}}})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'substitutePath' attribute '[map[to:path2]]' in debug configuration is not a []{'from': string, 'to': string}")
"Failed to launch: invalid debug configuration - 'substitutePath' requires both 'from' and 'to' entries")
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "substitutePath": []interface{}{map[string]interface{}{"from": "path1", "to": 123}}})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to launch: 'substitutePath' attribute '[map[from:path1 to:123]]' in debug configuration is not a []{'from': string, 'to': string}")
"Failed to launch: invalid debug configuration - cannot use {\"from\":\"path1\",\"to\":123} as 'substitutePath' of type {\"from\":string, \"to\":string}")
// Bad "cwd"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "cwd": 123})
checkFailedToLaunchWithMessage(client.ExpectErrorResponse(t),
"Failed to launch: 'cwd' attribute '123' in debug configuration is not a string.")
"Failed to launch: invalid debug configuration - cannot unmarshal number into \"cwd\" of type string")
// Skip detailed message checks for potentially different OS-specific errors.
client.LaunchRequest("exec", fixture.Path+"_does_not_exist", stopOnEntry)
@ -4960,7 +4944,12 @@ func TestBadLaunchRequests(t *testing.T) {
}
checkFailedToLaunch(client.ExpectInvisibleErrorResponse(t))
client.LaunchRequest("" /*debug by default*/, fixture.Path+"_does_not_exist", stopOnEntry)
client.LaunchRequestWithArgs(map[string]interface{}{
"request": "launch",
/* mode: debug by default*/
"program": fixture.Path + "_does_not_exist",
"stopOnEntry": stopOnEntry,
})
oe = client.ExpectOutputEvent(t)
if !strings.HasPrefix(oe.Body.Output, "Build Error: ") || oe.Body.Category != "stderr" {
t.Errorf("got %#v, want Category=\"stderr\" Output=\"Build Error: ...\"", oe)
@ -4989,6 +4978,9 @@ func TestBadLaunchRequests(t *testing.T) {
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "noDebug": true, "cwd": "dir/invalid"})
checkFailedToLaunch(client.ExpectErrorResponse(t)) // invalid directory, the error message is system-dependent.
// Bad "noDebug"
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "debug", "program": fixture.Source, "noDebug": "true"})
checkFailedToLaunchWithMessage(client.ExpectInvisibleErrorResponse(t), "Failed to launch: invalid debug configuration - cannot unmarshal string into \"noDebug\" of type bool")
// Bad "replay" parameters
// These errors come from dap layer
client.LaunchRequestWithArgs(map[string]interface{}{"mode": "replay", "traceDirPath": ""})
@ -5057,15 +5049,15 @@ func TestBadAttachRequest(t *testing.T) {
// Bad "mode"
client.AttachRequest(map[string]interface{}{"mode": "remote"})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: Unsupported 'mode' value \"remote\" in debug configuration")
"Failed to attach: invalid debug configuration - unsupported 'mode' attribute \"remote\"")
client.AttachRequest(map[string]interface{}{"mode": "blah blah blah"})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: Unsupported 'mode' value \"blah blah blah\" in debug configuration")
"Failed to attach: invalid debug configuration - unsupported 'mode' attribute \"blah blah blah\"")
client.AttachRequest(map[string]interface{}{"mode": 123})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: Unsupported 'mode' value %!q(float64=123) in debug configuration")
"Failed to attach: invalid debug configuration - cannot unmarshal number into \"mode\" of type string")
client.AttachRequest(map[string]interface{}{"mode": ""}) // empty mode defaults to "local" (not an error)
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
@ -5090,7 +5082,7 @@ func TestBadAttachRequest(t *testing.T) {
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": "1"})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: The 'processId' attribute is missing in debug configuration")
"Failed to attach: invalid debug configuration - cannot unmarshal string into \"processId\" of type int")
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 1})
// The exact message varies on different systems, so skip that check
@ -5116,13 +5108,10 @@ func TestBadAttachRequest(t *testing.T) {
// Bad "backend"
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 1, "backend": 123})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: 'backend' attribute '123' in debug configuration is not a string.")
"Failed to attach: invalid debug configuration - cannot unmarshal number into \"backend\" of type string")
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 1, "backend": "foo"})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: could not attach to pid 1: unknown backend \"foo\"")
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 1, "backend": ""})
checkFailedToAttachWithMessage(client.ExpectInvisibleErrorResponse(t),
"Failed to attach: could not attach to pid 1: unknown backend \"\"")
// We failed to attach to the program. Make sure shutdown still works.
client.DisconnectRequest()

213
service/dap/types.go Normal file

@ -0,0 +1,213 @@
package dap
import (
"encoding/json"
"errors"
"fmt"
)
// Launch debug sessions support the following modes:
// -- [DEFAULT] "debug" - builds and launches debugger for specified program (similar to 'dlv debug')
// Required args: program
// Optional args with default: output, cwd, noDebug
// Optional args: buildFlags, args
// -- "test" - builds and launches debugger for specified test (similar to 'dlv test')
// same args as above
// -- "exec" - launches debugger for precompiled binary (similar to 'dlv exec')
// Required args: program
// Optional args with default: cwd, noDebug
// Optional args: args
// -- "replay" - replays a trace generated by mozilla rr. Mozilla rr must be installed.
// Required args: traceDirPath
// Optional args: args
// -- "core" - examines a core dump (only supports linux and windows core dumps).
// Required args: program, coreFilePath
// Optional args: args
//
// TODO(hyangah): change this to 'validateLaunchMode' that checks
// all the required/optional fields mentioned above.
func isValidLaunchMode(mode string) bool {
switch mode {
case "exec", "debug", "test", "replay", "core":
return true
}
return false
}
// Attach debug sessions support the following modes:
// -- [DEFAULT] "local" -- attaches debugger to a local running process
// Required args: processID
// TODO(polina): support "remote" mode
func isValidAttachMode(mode string) bool {
return mode == "local"
}
// Default values for Launch/Attach configs.
// Used to initialize configuration variables before decoding
// arguments in launch/attach requests.
var (
defaultLaunchAttachCommonConfig = LaunchAttachCommonConfig{
Backend: "default",
StackTraceDepth: 50,
}
defaultLaunchConfig = LaunchConfig{
Mode: "debug",
Output: defaultDebugBinary,
LaunchAttachCommonConfig: defaultLaunchAttachCommonConfig,
}
defaultAttachConfig = AttachConfig{
Mode: "local",
LaunchAttachCommonConfig: defaultLaunchAttachCommonConfig,
}
)
// LaunchConfig is the collection of launch request attributes recognized by delve DAP implementation.
type LaunchConfig struct {
// Acceptable values are:
// "debug": compiles your program with optimizations disabled, starts and attaches to it.
// "test": compiles your unit test program with optimizations disabled, starts and attaches to it.
// "exec": executes a precompiled binary and begins a debug session.
// "replay": replays an rr trace.
// "core": examines a core dump.
//
// Default is "debug".
Mode string `json:"mode,omitempty"`
// Required when mode is `debug`, `test`, or `exec`.
// Path to the program folder (or any go file within that folder)
// when in `debug` or `test` mode, and to the pre-built binary file
// to debug in `exec` mode.
// If it is not an absolute path, it will be interpreted as a path
// relative to the working directory of the delve process.
Program string `json:"program,omitempty"`
// Command line arguments passed to the debugged program.
Args []string `json:"args,omitempty"`
// Working directory of the program being debugged
// if a non-empty value is specified. If a relative path is provided,
// it will be interpreted as a relative path to the delve's
// working directory.
//
// If not specified or empty, currently the built program's directory will
// be used.
// This is similar to delve's `--wd` flag.
Cwd string `json:"cwd,omitempty"`
// Build flags, to be passed to the Go compiler.
// It is like delve's `--build-flags`. For example,
//
// "buildFlags": "-tags=integration -mod=vendor -cover -v"
BuildFlags string `json:"buildFlags,omitempty"`
// Output path for the binary of the debugee.
// Relative path is interpreted as the path relative to
// the delve process's working directory.
// This is deleted after the debug session ends.
//
// FIXIT: the built program's directory is used as the default
// working directory of the debugged program, which means
// the directory of `output` is used as the default working
// directory. This is a bug and needs fix.
Output string `json:"output,omitempty"`
// NoDebug is used to run the program without debugging.
NoDebug bool `json:"noDebug,omitempty"`
// TraceDirPath is the trace directory path for replay mode.
// This is required for "replay" mode but unused in other modes.
TraceDirPath string `json:"traceDirPath,omitempty"`
// CoreFilePath is the core file path for core mode.
// This is required for "core" mode but unused in other modes.
CoreFilePath string `json:"coreFilePath,omitempty"`
LaunchAttachCommonConfig
}
// LaunchAttachCommonConfig is the attributes common in both launch/attach requests.
type LaunchAttachCommonConfig struct {
// Automatically stop program after launch or attach.
StopOnEntry bool `json:"stopOnEntry,omitempty"`
// Backend used by delve. See `dlv backend` for allowed values.
// Default is "default".
Backend string `json:"backend,omitempty"`
// Maximum depth of stack trace collected from Delve.
// Default is 50.
StackTraceDepth int `json:"stackTraceDepth,omitempty"`
// Boolean value to indicate whether global package variables
// should be shown in the variables pane or not.
ShowGlobalVariables bool `json:"showGlobalVariables,omitempty"`
// An array of mappings from a local path (client) to the remote path (debugger).
// This setting is useful when working in a file system with symbolic links,
// running remote debugging, or debugging an executable compiled externally.
// The debug adapter will replace the local path with the remote path in all of the calls.
SubstitutePath []SubstitutePath `json:"substitutePath,omitempty"`
}
// SubstitutePath defines a mapping from a local path to the remote path.
// Both 'from' and 'to' must be specified and non-empty.
type SubstitutePath struct {
// The local path to be replaced when passing paths to the debugger.
From string `json:"from,omitempty"`
// The remote path to be replaced when passing paths back to the client.
To string `json:"to,omitempty"`
}
func (m *SubstitutePath) UnmarshalJSON(data []byte) error {
// use custom unmarshal to check if both from/to are set.
type tmpType SubstitutePath
var tmp tmpType
if err := json.Unmarshal(data, &tmp); err != nil {
if _, ok := err.(*json.UnmarshalTypeError); ok {
return fmt.Errorf(`cannot use %s as 'substitutePath' of type {"from":string, "to":string}`, data)
}
return err
}
if tmp.From == "" || tmp.To == "" {
return errors.New("'substitutePath' requires both 'from' and 'to' entries")
}
*m = SubstitutePath(tmp)
return nil
}
// AttachConfig is the collection of attach request attributes recognized by delve DAP implementation.
type AttachConfig struct {
// Acceptable values are:
// "local": attaches to the local process with the given ProcessID.
//
// Default is "local".
Mode string `json:"mode"`
// The numeric ID of the process to be debugged. Required and must not be 0.
ProcessID int `json:"processId,omitempty"`
LaunchAttachCommonConfig
}
// unmarshalLaunchAttachArgs wraps unmarshalling of launch/attach request's
// arguments attribute. Upon unmarshal failure, it returns an error massaged
// to be suitable for end-users.
func unmarshalLaunchAttachArgs(input json.RawMessage, config interface{}) error {
if err := json.Unmarshal(input, config); err != nil {
if uerr, ok := err.(*json.UnmarshalTypeError); ok {
// Format json.UnmarshalTypeError error string in our own way. E.g.,
// "json: cannot unmarshal number into Go struct field LaunchArgs.substitutePath of type dap.SubstitutePath"
// => "cannot unmarshal number into 'substitutePath' of type {from:string, to:string}"
// "json: cannot unmarshal number into Go struct field LaunchArgs.program of type string" (go1.16)
// => "cannot unmarshal number into 'program' of type string"
typ := uerr.Type.String()
if uerr.Field == "substitutePath" {
typ = `{"from":string, "to":string}`
}
return fmt.Errorf("cannot unmarshal %v into %q of type %v", uerr.Value, uerr.Field, typ)
}
return err
}
return nil
}

@ -6,6 +6,7 @@ language: go
go:
- 1.14.x
- 1.15.x
- 1.16.x
env:
global:

@ -1,5 +1,6 @@
# go-dap: Go implementation of the Debug Adapter Protocol
[![PkgGoDev](https://pkg.go.dev/badge/github.com/google/go-dap)](https://pkg.go.dev/github.com/google/go-dap)
[![Build Status](https://travis-ci.org/google/go-dap.svg?branch=master)](https://travis-ci.org/google/go-dap)
[![Go Report Card](https://goreportcard.com/badge/github.com/google/go-dap)](https://goreportcard.com/report/github.com/google/go-dap)

@ -18,6 +18,8 @@
package dap
import "encoding/json"
// Message is an interface that all DAP message types implement with pointer
// receivers. It's not part of the protocol but is used to enforce static
// typing in Go code and provide some common accessors.
@ -56,7 +58,7 @@ type EventMessage interface {
type LaunchAttachRequest interface {
RequestMessage
// GetArguments provides access to the Arguments map.
GetArguments() map[string]interface{}
GetArguments() json.RawMessage
}
// ProtocolMessage: Base class of requests, responses, and events.
@ -492,11 +494,11 @@ func (r *ConfigurationDoneResponse) GetResponse() *Response { return &r.Response
type LaunchRequest struct {
Request
Arguments map[string]interface{} `json:"arguments"`
Arguments json.RawMessage `json:"arguments"`
}
func (r *LaunchRequest) GetRequest() *Request { return &r.Request }
func (r *LaunchRequest) GetArguments() map[string]interface{} { return r.Arguments }
func (r *LaunchRequest) GetRequest() *Request { return &r.Request }
func (r *LaunchRequest) GetArguments() json.RawMessage { return r.Arguments }
// LaunchResponse: Response to 'launch' request. This is just an acknowledgement, so no body field is required.
type LaunchResponse struct {
@ -510,11 +512,11 @@ func (r *LaunchResponse) GetResponse() *Response { return &r.Response }
type AttachRequest struct {
Request
Arguments map[string]interface{} `json:"arguments"`
Arguments json.RawMessage `json:"arguments"`
}
func (r *AttachRequest) GetRequest() *Request { return &r.Request }
func (r *AttachRequest) GetArguments() map[string]interface{} { return r.Arguments }
func (r *AttachRequest) GetRequest() *Request { return &r.Request }
func (r *AttachRequest) GetArguments() json.RawMessage { return r.Arguments }
// AttachResponse: Response to 'attach' request. This is just an acknowledgement, so no body field is required.
type AttachResponse struct {

2
vendor/modules.txt vendored

@ -13,7 +13,7 @@ github.com/creack/pty
# github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9
## explicit
github.com/derekparker/trie
# github.com/google/go-dap v0.5.0
# github.com/google/go-dap v0.5.1-0.20210713061233-c91b005e3987
## explicit
github.com/google/go-dap
# github.com/hashicorp/golang-lru v0.5.4