whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
The schema for DAP messages is defined in JSON at
|
||||
https://github.com/microsoft/vscode-debugadapter-node/blob/main/debugProtocol.json
|
||||
|
||||
The auto-generated TypeScript representation of the schema is at
|
||||
https://github.com/microsoft/vscode-debugadapter-node/blob/main/protocol/src/debugProtocol.ts
|
||||
|
||||
----
|
||||
|
||||
In this directory we have a copy of the schema, which is licensed by Microsoft
|
||||
with a [MIT
|
||||
License](https://github.com/microsoft/vscode-debugadapter-node/blob/main/License.txt).
|
||||
This copy must be updated whenever the schema changes.
|
||||
|
||||
To generate Go types from the schema, run:
|
||||
|
||||
```
|
||||
$ go run cmd/gentypes/gentypes.go cmd/gentypes/debugProtocol.json > schematypes.go
|
||||
```
|
||||
|
||||
The generated ``schematypes.go`` is also checked in, so there is no need to
|
||||
regenerate it unless the schema changes.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,703 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// gentypes generates Go types from debugProtocol.json
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// $ gentypes <path to debugProtocol.json>
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
uFlag = flag.Bool("u", false, "updates the debugProtocol.json file before generating the code")
|
||||
oFlag = flag.String("o", "", "specifies the output file name. If unspecified, outputs to stdout")
|
||||
)
|
||||
|
||||
// parseRef parses the value of a "$ref" key.
|
||||
// For example "#definitions/ProtocolMessage" => "ProtocolMessage".
|
||||
func parseRef(refValue any) string {
|
||||
refContents := refValue.(string)
|
||||
if !strings.HasPrefix(refContents, "#/definitions/") {
|
||||
log.Fatal("want ref to start with '#/definitions/', got ", refValue)
|
||||
}
|
||||
|
||||
return replaceGoTypename(refContents[14:])
|
||||
}
|
||||
|
||||
// goFieldName converts a property name from its JSON representation to an
|
||||
// exported Go field name.
|
||||
// For example "__some_property_name" => "SomePropertyName".
|
||||
func goFieldName(jsonPropName string) string {
|
||||
var ret strings.Builder
|
||||
ret.Grow(len(jsonPropName))
|
||||
upper := true
|
||||
for _, r := range jsonPropName {
|
||||
switch {
|
||||
case r == '_':
|
||||
upper = true
|
||||
case upper:
|
||||
upper = false
|
||||
ret.WriteRune(unicode.ToUpper(r))
|
||||
default:
|
||||
ret.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
// parsePropertyType takes the JSON value of a property field and extracts
|
||||
// the Go type of the property. For example, given this map:
|
||||
//
|
||||
// {
|
||||
// "type": "string",
|
||||
// "description": "The command to execute."
|
||||
// },
|
||||
//
|
||||
// It will emit "string".
|
||||
func parsePropertyType(propValue map[string]any) string {
|
||||
if ref, ok := propValue["$ref"]; ok {
|
||||
return parseRef(ref)
|
||||
}
|
||||
|
||||
if _, ok := propValue["oneOf"]; ok {
|
||||
return "any"
|
||||
}
|
||||
propType, ok := propValue["type"]
|
||||
if !ok {
|
||||
log.Fatal("property with no type or ref:", propValue)
|
||||
}
|
||||
|
||||
switch typ := propType.(type) {
|
||||
case string:
|
||||
switch typ {
|
||||
case "string":
|
||||
return "string"
|
||||
case "number":
|
||||
return "int"
|
||||
case "integer":
|
||||
return "int"
|
||||
case "boolean":
|
||||
return "bool"
|
||||
case "array":
|
||||
propItems, ok := propValue["items"]
|
||||
if !ok {
|
||||
log.Fatal("missing items type for property of array type:", propValue)
|
||||
}
|
||||
propItemsMap := propItems.(map[string]any)
|
||||
return "[]" + parsePropertyType(propItemsMap)
|
||||
case "object":
|
||||
// When the type of a property is "object", we'll emit a map with a string
|
||||
// key and a value type that depends on the type of the
|
||||
// additionalProperties field.
|
||||
additionalProps, ok := propValue["additionalProperties"]
|
||||
if !ok {
|
||||
log.Fatal("missing additionalProperties field when type=object:", propValue)
|
||||
}
|
||||
var valueType string
|
||||
switch actual := additionalProps.(type) {
|
||||
case bool:
|
||||
valueType = "any"
|
||||
case map[string]any:
|
||||
valueType = parsePropertyType(actual)
|
||||
default:
|
||||
log.Fatal("unexpected additionalProperties value:", additionalProps)
|
||||
}
|
||||
return fmt.Sprintf("map[string]%v", valueType)
|
||||
case "any":
|
||||
return "any"
|
||||
default:
|
||||
log.Fatalf("unknown property type value %v in %v", propType, propValue)
|
||||
}
|
||||
|
||||
case []any:
|
||||
// This field is polymorphic so it needs a generic type.
|
||||
for _, el := range typ {
|
||||
s, ok := el.(string)
|
||||
if !ok {
|
||||
log.Fatalf("property type contains a non-string of type %T: %#v", el, typ)
|
||||
}
|
||||
if s == "object" || s == "array" {
|
||||
// It contains non-fundamental types, so treat it as opaque.
|
||||
return "json.RawMessage"
|
||||
}
|
||||
}
|
||||
// The possible types are all fundamental types, so we can use any.
|
||||
return "any"
|
||||
|
||||
default:
|
||||
log.Fatalf("unknown property type %T (%#v)", typ, typ)
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// maybeParseInheritance helps parse types that inherit from other types.
|
||||
// A type description can have an "allOf" key, which means it inherits from
|
||||
// another type description. Returns the name of the base type specified in
|
||||
// allOf, and the description of the inheriting type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "allOf": [ { "$ref": "#/definitions/ProtocolMessage" },
|
||||
// {... type description ...} ]
|
||||
//
|
||||
// Returns base type ProtocolMessage and a map representing type description.
|
||||
// If there is no "allOf", returns an empty baseTypeName and descMap itself.
|
||||
func maybeParseInheritance(descMap map[string]json.RawMessage) (baseTypeName string, typeDescJson map[string]json.RawMessage) {
|
||||
allOfListJson, ok := descMap["allOf"]
|
||||
if !ok {
|
||||
return "", descMap
|
||||
}
|
||||
|
||||
var allOfSliceOfJson []json.RawMessage
|
||||
if err := json.Unmarshal(allOfListJson, &allOfSliceOfJson); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(allOfSliceOfJson) != 2 {
|
||||
log.Fatal("want 2 elements in allOf list, got", allOfSliceOfJson)
|
||||
}
|
||||
|
||||
var baseTypeRef map[string]any
|
||||
if err := json.Unmarshal(allOfSliceOfJson[0], &baseTypeRef); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(allOfSliceOfJson[1], &typeDescJson); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return parseRef(baseTypeRef["$ref"]), typeDescJson
|
||||
}
|
||||
|
||||
// emitToplevelType emits a single type into a string. It takes the type name
|
||||
// and a serialized json object representing the type. The json representation
|
||||
// will have fields: "type", "properties" etc.
|
||||
func emitToplevelType(typeName string, descJson json.RawMessage, goTypeIsStruct map[string]bool) string {
|
||||
var b strings.Builder
|
||||
var baseType string
|
||||
|
||||
// We don't parse the description all the way to map[string]any
|
||||
// because we have to retain the original JSON-order of properties (in this
|
||||
// type as well as any nested types like "body").
|
||||
var descMap map[string]json.RawMessage
|
||||
if err := json.Unmarshal(descJson, &descMap); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
baseType, descMap = maybeParseInheritance(descMap)
|
||||
|
||||
typeJson, ok := descMap["type"]
|
||||
if !ok {
|
||||
log.Fatal("want description to have 'type', got ", descMap)
|
||||
}
|
||||
|
||||
var descTypeString string
|
||||
if err := json.Unmarshal(typeJson, &descTypeString); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var comment string
|
||||
descriptionJson, ok := descMap["description"]
|
||||
if ok {
|
||||
if err := json.Unmarshal(descriptionJson, &comment); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(comment) > 0 {
|
||||
comment = commentOutEachLine(fmt.Sprintf("%s: %s", typeName, comment))
|
||||
fmt.Fprint(&b, comment)
|
||||
}
|
||||
|
||||
if descTypeString == "string" {
|
||||
fmt.Fprintf(&b, "type %s string\n", typeName)
|
||||
return b.String()
|
||||
} else if descTypeString == "object" {
|
||||
fmt.Fprintf(&b, "type %s struct {\n", typeName)
|
||||
if len(baseType) > 0 {
|
||||
fmt.Fprintf(&b, "\t%s\n\n", baseType)
|
||||
}
|
||||
} else {
|
||||
log.Fatal("want description type to be object or string, got ", descTypeString)
|
||||
}
|
||||
|
||||
var propsMapOfJson map[string]json.RawMessage
|
||||
if propsJson, ok := descMap["properties"]; ok {
|
||||
if err := json.Unmarshal(propsJson, &propsMapOfJson); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
b.WriteString("}\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
propsNamesInOrder, err := keysInOrder(descMap["properties"])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Stores the properties that are required.
|
||||
requiredMap := make(map[string]bool)
|
||||
|
||||
if requiredJson, ok := descMap["required"]; ok {
|
||||
var required []any
|
||||
if err := json.Unmarshal(requiredJson, &required); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, r := range required {
|
||||
requiredMap[r.(string)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Some types will have a "body" which should be emitted as a separate type.
|
||||
// Since we can't emit a whole new Go type while in the middle of emitting
|
||||
// another type, we save it for later and emit it after the current type is
|
||||
// done.
|
||||
bodyType := ""
|
||||
|
||||
for _, propName := range propsNamesInOrder {
|
||||
// The JSON schema is designed for the TypeScript type system, where a
|
||||
// subclass can redefine a field in a superclass with a refined type (such
|
||||
// as specific values for a field). To ensure we emit Go structs that can
|
||||
// be unmarshaled from JSON messages properly, we must limit each field
|
||||
// to appear only once in hierarchical types.
|
||||
if propName == "type" && (typeName == "Request" || typeName == "Response" || typeName == "Event") {
|
||||
continue
|
||||
}
|
||||
if propName == "command" && typeName != "Request" && typeName != "Response" {
|
||||
continue
|
||||
}
|
||||
if propName == "event" && typeName != "Event" {
|
||||
continue
|
||||
}
|
||||
if propName == "arguments" && typeName == "Request" {
|
||||
continue
|
||||
}
|
||||
|
||||
var propDesc map[string]any
|
||||
if err := json.Unmarshal(propsMapOfJson[propName], &propDesc); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if propName == "body" {
|
||||
if typeName == "Response" || typeName == "Event" {
|
||||
continue
|
||||
}
|
||||
|
||||
var bodyTypeName string
|
||||
if ref, ok := propDesc["$ref"]; ok {
|
||||
bodyTypeName = parseRef(ref)
|
||||
} else {
|
||||
bodyTypeName = typeName + "Body"
|
||||
bodyType = emitToplevelType(bodyTypeName, propsMapOfJson["body"], goTypeIsStruct)
|
||||
}
|
||||
|
||||
if requiredMap["body"] {
|
||||
fmt.Fprintf(&b, "\t%s %s `json:\"body\"`\n", "Body", bodyTypeName)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "\t%s %s `json:\"body,omitempty\"`\n", "Body", bodyTypeName)
|
||||
}
|
||||
} else if propName == "arguments" && (typeName == "LaunchRequest" || typeName == "AttachRequest" || typeName == "RestartRequest") {
|
||||
// Special case for LaunchRequest or AttachRequest arguments, which are implementation
|
||||
// defined and don't have pre-set field names in the specification.
|
||||
fmt.Fprintln(&b, "\tArguments json.RawMessage `json:\"arguments\"`")
|
||||
} else {
|
||||
// Go type of this property.
|
||||
goType := parsePropertyType(propDesc)
|
||||
|
||||
jsonTag := fmt.Sprintf("`json:\"%s", propName)
|
||||
if requiredMap[propName] {
|
||||
jsonTag += "\"`"
|
||||
} else if typeName == "ContinueResponseBody" && propName == "allThreadsContinued" {
|
||||
// This one special field must not have the omitempty tag, despite being
|
||||
// optional. If this attribute is missing the client will (according to
|
||||
// the specification) assume a value of 'true' for backward
|
||||
// compatibility. See: https://github.com/google/go-dap/issues/39
|
||||
jsonTag += "\"`"
|
||||
} else if typeName == "InitializeRequestArguments" && (propName == "linesStartAt1" || propName == "columnsStartAt1") {
|
||||
// These two special fields must not have the omitempty tag, despite being
|
||||
// optional. If this attribute is missing the server will (according to
|
||||
// the specification) assume a value of 'true'.
|
||||
jsonTag += "\"`"
|
||||
} else if typeName == "ErrorMessage" && propName == "showUser" {
|
||||
// For launch/attach errors, vscode will treat omitted values the same way as true,
|
||||
// so to suppress visible reporting, we must report false explicitly.
|
||||
jsonTag += "\"`"
|
||||
} else {
|
||||
jsonTag += ",omitempty\"`"
|
||||
// If the field should be omitted when empty and is a struct type in Go, make it a pointer,
|
||||
// because non-pointer structs get initialized with default values in Go (and not nil), and
|
||||
// are then indistinguishable from structs with values actually set to zero when serializing
|
||||
// to JSON. Making them a pointer makes them initialize to nil, which is then indeed omitted
|
||||
// during serialization.
|
||||
if _, ok := propDesc["$ref"]; ok {
|
||||
// If we have a ref, then goType is the parsed ref
|
||||
if goTypeIsStruct[goType] {
|
||||
goType = "*" + goType
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "\t%s %s %s\n", goFieldName(propName), goType, jsonTag)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("}\n")
|
||||
|
||||
if len(bodyType) > 0 {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(bodyType)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// keysInOrder returns the keys in json object in b, in their original order.
|
||||
// Based on https://github.com/golang/go/issues/27179#issuecomment-415559968
|
||||
func keysInOrder(b []byte) ([]string, error) {
|
||||
d := json.NewDecoder(bytes.NewReader(b))
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t != json.Delim('{') {
|
||||
return nil, errors.New("expected start of object")
|
||||
}
|
||||
var keys []string
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t == json.Delim('}') {
|
||||
return keys, nil
|
||||
}
|
||||
keys = append(keys, t.(string))
|
||||
if err := skipValue(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replaceGoTypename replaces conflicting type names in the JSON schema with
|
||||
// proper Go type names.
|
||||
func replaceGoTypename(typeName string) string {
|
||||
// Since we have a top-level interface named Message, we replace the DAP
|
||||
// message type Message with ErrorMessage.
|
||||
if typeName == "Message" {
|
||||
return "ErrorMessage"
|
||||
}
|
||||
return typeName
|
||||
}
|
||||
|
||||
var errEnd = errors.New("invalid end of array or object")
|
||||
|
||||
func skipValue(d *json.Decoder) error {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t {
|
||||
case json.Delim('['), json.Delim('{'):
|
||||
for {
|
||||
if err := skipValue(d); err != nil {
|
||||
if err == errEnd {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
case json.Delim(']'), json.Delim('}'):
|
||||
return errEnd
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commentOutEachLine returns s such that a Go comment marker ("//") is
|
||||
// prepended to each line.
|
||||
func commentOutEachLine(s string) string {
|
||||
parts := strings.Split(s, "\n")
|
||||
var sb strings.Builder
|
||||
|
||||
for _, p := range parts {
|
||||
fmt.Fprintf(&sb, "// %s\n", p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// emitMethodsForType may emit methods for typeName into sb.
|
||||
func emitMethodsForType(sb *strings.Builder, typeName string) {
|
||||
if typeName == "ProtocolMessage" {
|
||||
fmt.Fprintln(sb, "func (m *ProtocolMessage) GetSeq() int {return m.Seq}")
|
||||
}
|
||||
if typeName == "Request" {
|
||||
fmt.Fprintln(sb, "func (r *Request) GetRequest() *Request {return r}")
|
||||
}
|
||||
if typeName == "Response" {
|
||||
fmt.Fprintln(sb, "func (r *Response) GetResponse() *Response {return r}")
|
||||
}
|
||||
if typeName == "Event" {
|
||||
fmt.Fprintln(sb, "func (e *Event) GetEvent() *Event {return e}")
|
||||
}
|
||||
if typeName == "LaunchRequest" || typeName == "AttachRequest" {
|
||||
fmt.Fprintf(sb, "func (r *%s) GetArguments() json.RawMessage { return r.Arguments }\n", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
func emitCtor(sb *strings.Builder, reqs, resps, events []string) {
|
||||
fmt.Fprint(sb, `
|
||||
// Mapping of request commands and corresponding struct constructors that
|
||||
// can be passed to json.Unmarshal.
|
||||
var requestCtor = map[string]messageCtor{`)
|
||||
for _, r := range reqs {
|
||||
req := strings.TrimSuffix(firstToLower(r), "Request")
|
||||
var msg string
|
||||
if req == "initialize" {
|
||||
msg = `
|
||||
Arguments: InitializeRequestArguments{
|
||||
// Set the default values specified here: https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize.
|
||||
LinesStartAt1: true,
|
||||
ColumnsStartAt1: true,
|
||||
PathFormat: "path",
|
||||
},
|
||||
`
|
||||
}
|
||||
fmt.Fprintf(sb, "\n\t\"%s\":\tfunc() Message { return &%s{%s} },", req, r, msg)
|
||||
}
|
||||
fmt.Fprint(sb, "\n}")
|
||||
|
||||
fmt.Fprint(sb, `
|
||||
// Mapping of response commands and corresponding struct constructors that
|
||||
// can be passed to json.Unmarshal.
|
||||
var responseCtor = map[string]messageCtor{`)
|
||||
for _, r := range resps {
|
||||
resp := strings.TrimSuffix(firstToLower(r), "Response")
|
||||
|
||||
fmt.Fprintf(sb, "\n\t\"%s\":\tfunc() Message { return &%s{} },", resp, r)
|
||||
}
|
||||
fmt.Fprint(sb, "\n}")
|
||||
|
||||
fmt.Fprint(sb, `
|
||||
// Mapping of event ids and corresponding struct constructors that
|
||||
// can be passed to json.Unmarshal.
|
||||
var eventCtor = map[string]messageCtor{`)
|
||||
for _, e := range events {
|
||||
ev := strings.TrimSuffix(firstToLower(e), "Event")
|
||||
fmt.Fprintf(sb, "\n\t\"%s\":\tfunc() Message { return &%s{} },", ev, e)
|
||||
}
|
||||
fmt.Fprint(sb, "\n}\n")
|
||||
}
|
||||
|
||||
func firstToLower(s string) string {
|
||||
r := []rune(s)
|
||||
return string(unicode.ToLower(r[0])) + string(r[1:])
|
||||
}
|
||||
|
||||
const preamble = `// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by "cmd/gentypes/gentypes.go"; DO NOT EDIT.
|
||||
// DAP spec: https://microsoft.github.io/debug-adapter-protocol/specification
|
||||
// See cmd/gentypes/README.md for additional details.
|
||||
|
||||
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.
|
||||
//
|
||||
// Note: the DAP type "Message" (which is used in the body of ErrorResponse)
|
||||
// is renamed to ErrorMessage to avoid collision with this interface.
|
||||
type Message interface {
|
||||
GetSeq() int
|
||||
}
|
||||
|
||||
// RequestMessage is an interface implemented by all Request-types.
|
||||
type RequestMessage interface {
|
||||
Message
|
||||
// GetRequest provides access to the embedded Request.
|
||||
GetRequest() *Request
|
||||
}
|
||||
|
||||
// ResponseMessage is an interface implemented by all Response-types.
|
||||
type ResponseMessage interface {
|
||||
Message
|
||||
// GetResponse provides access to the embedded Response.
|
||||
GetResponse() *Response
|
||||
}
|
||||
|
||||
// EventMessage is an interface implemented by all Event-types.
|
||||
type EventMessage interface {
|
||||
Message
|
||||
// GetEvent provides access to the embedded Event.
|
||||
GetEvent() *Event
|
||||
}
|
||||
|
||||
// LaunchAttachRequest is an interface implemented by
|
||||
// LaunchRequest and AttachRequest as they contain shared
|
||||
// implementation specific arguments that are not part of
|
||||
// the specification.
|
||||
type LaunchAttachRequest interface {
|
||||
RequestMessage
|
||||
// GetArguments provides access to the Arguments map.
|
||||
GetArguments() json.RawMessage
|
||||
}
|
||||
`
|
||||
|
||||
// typesExcludeList is an exclude list of type names we don't want to emit.
|
||||
var typesExcludeList = map[string]bool{
|
||||
// LaunchRequest and AttachRequest arguments can be arbitrary maps.
|
||||
// Therefore, this type is not used anywhere.
|
||||
"LaunchRequestArguments": true,
|
||||
"AttachRequestArguments": true,
|
||||
"RestartArguments": true,
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Path to the DAP specification json file is required.")
|
||||
fmt.Fprintln(os.Stderr, "gentypes <path/to/debugProtocol.json>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputFilename := flag.Arg(0)
|
||||
|
||||
if *uFlag {
|
||||
if err := updateInput(inputFilename); err != nil {
|
||||
log.Fatalf("Failed to update the input file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
inputData, err := ioutil.ReadFile(inputFilename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(inputData, &m); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var typeMap map[string]json.RawMessage
|
||||
if err := json.Unmarshal(m["definitions"], &typeMap); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
goTypesIsStruct := make(map[string]bool)
|
||||
for typeName, descJson := range typeMap {
|
||||
var descMap map[string]json.RawMessage
|
||||
if err := json.Unmarshal(descJson, &descMap); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, descMap = maybeParseInheritance(descMap)
|
||||
|
||||
typeJson, ok := descMap["type"]
|
||||
if !ok {
|
||||
log.Fatal("want description to have 'type', got ", descMap)
|
||||
}
|
||||
|
||||
var descTypeString string
|
||||
if err := json.Unmarshal(typeJson, &descTypeString); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
goTypesIsStruct[replaceGoTypename(typeName)] = descTypeString == "object"
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(preamble)
|
||||
|
||||
typeNames, err := keysInOrder(m["definitions"])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var requests, responses, events []string
|
||||
for _, typeName := range typeNames {
|
||||
if _, ok := typesExcludeList[typeName]; !ok {
|
||||
b.WriteString(emitToplevelType(replaceGoTypename(typeName), typeMap[typeName], goTypesIsStruct))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
emitMethodsForType(&b, replaceGoTypename(typeName))
|
||||
// Add the typename to the appropriate list.
|
||||
if strings.HasSuffix(typeName, "Request") && typeName != "Request" {
|
||||
requests = append(requests, typeName)
|
||||
}
|
||||
if strings.HasSuffix(typeName, "Response") && typeName != "Response" && typeName != "ErrorResponse" {
|
||||
responses = append(responses, typeName)
|
||||
}
|
||||
if strings.HasSuffix(typeName, "Event") && typeName != "Event" {
|
||||
events = append(events, typeName)
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the maps from id to response and event types.
|
||||
emitCtor(&b, requests, responses, events)
|
||||
|
||||
wholeFile := []byte(b.String())
|
||||
formatted, err := format.Source(wholeFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if *oFlag == "" {
|
||||
fmt.Print(string(formatted))
|
||||
} else {
|
||||
if err := ioutil.WriteFile(*oFlag, formatted, 0644); err != nil {
|
||||
log.Fatalf("Failed to write the generated file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateInput(inputFilename string) error {
|
||||
resp, err := http.Get("https://raw.githubusercontent.com/microsoft/vscode-debugadapter-node/main/debugProtocol.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(inputFilename, data, 0644)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This starts a mock DAP server that runs indefinitely, accepts DAP
|
||||
// requests and responds with dummy or error responses.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.String("port", "54321", "TCP port to listen on")
|
||||
flag.Parse()
|
||||
err := server(*port)
|
||||
if err != nil {
|
||||
log.Fatal("Could not start server: ", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file defines helpers and request handlers for a dummy server
|
||||
// that accepts DAP requests and responds with dummy or error responses.
|
||||
// Fake-supported requests:
|
||||
// - initialize
|
||||
// - launch
|
||||
// - setBreakpoints
|
||||
// - setExceptionBreakpoints
|
||||
// - configurationDone
|
||||
// - threads
|
||||
// - stackTrace
|
||||
// - scopes
|
||||
// - variables
|
||||
// - continue
|
||||
// - disconnect
|
||||
// All other requests result in ErrorResponse's.
|
||||
//
|
||||
// The server uses the following goroutines:
|
||||
// - "main" goroutine accepts client connections one by one and
|
||||
// handles them serially by reading and decoding incoming requests
|
||||
// and dispatching each one to a new goroutine for further
|
||||
// processing.
|
||||
// - per-request goroutines process each request as if
|
||||
// letting fake debugger take over. They send events and responses
|
||||
// via the sender goroutine.
|
||||
// - sender goroutine listens for messages to send and
|
||||
// writes them to the client connection.
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
)
|
||||
|
||||
// server starts a server that listens on a specified port
|
||||
// and blocks indefinitely. This server can accept multiple
|
||||
// client connections at the same time.
|
||||
func server(port string) error {
|
||||
listener, err := net.Listen("tcp", ":"+port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
log.Println("Started server at", listener.Addr())
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Println("Connection failed:", err)
|
||||
continue
|
||||
}
|
||||
log.Println("Accepted connection from", conn.RemoteAddr())
|
||||
// Handle multiple client connections concurrently
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection handles a connection from a single client.
|
||||
// It reads and decodes the incoming data and dispatches it
|
||||
// to per-request processing goroutines. It also launches the
|
||||
// sender goroutine to send resulting messages over the connection
|
||||
// back to the client.
|
||||
func handleConnection(conn net.Conn) {
|
||||
debugSession := fakeDebugSession{
|
||||
rw: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)),
|
||||
sendQueue: make(chan dap.Message),
|
||||
stopDebug: make(chan struct{}),
|
||||
}
|
||||
go debugSession.sendFromQueue()
|
||||
|
||||
for {
|
||||
err := debugSession.handleRequest()
|
||||
// TODO(polina): check for connection vs decoding error?
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
log.Println("No more data to read:", err)
|
||||
break
|
||||
}
|
||||
// There maybe more messages to process, but
|
||||
// we will start with the strict behavior of only accepting
|
||||
// expected inputs.
|
||||
log.Fatal("Server error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Closing connection from", conn.RemoteAddr())
|
||||
close(debugSession.stopDebug)
|
||||
debugSession.sendWg.Wait()
|
||||
close(debugSession.sendQueue)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) handleRequest() error {
|
||||
log.Println("Reading request...")
|
||||
request, err := dap.ReadProtocolMessage(ds.rw.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Received request\n\t%#v\n", request)
|
||||
ds.sendWg.Add(1)
|
||||
go func() {
|
||||
ds.dispatchRequest(request)
|
||||
ds.sendWg.Done()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchRequest launches a new goroutine to process each request
|
||||
// and send back events and responses.
|
||||
func (ds *fakeDebugSession) dispatchRequest(request dap.Message) {
|
||||
switch request := request.(type) {
|
||||
case *dap.InitializeRequest:
|
||||
ds.onInitializeRequest(request)
|
||||
case *dap.LaunchRequest:
|
||||
ds.onLaunchRequest(request)
|
||||
case *dap.AttachRequest:
|
||||
ds.onAttachRequest(request)
|
||||
case *dap.DisconnectRequest:
|
||||
ds.onDisconnectRequest(request)
|
||||
case *dap.TerminateRequest:
|
||||
ds.onTerminateRequest(request)
|
||||
case *dap.RestartRequest:
|
||||
ds.onRestartRequest(request)
|
||||
case *dap.SetBreakpointsRequest:
|
||||
ds.onSetBreakpointsRequest(request)
|
||||
case *dap.SetFunctionBreakpointsRequest:
|
||||
ds.onSetFunctionBreakpointsRequest(request)
|
||||
case *dap.SetExceptionBreakpointsRequest:
|
||||
ds.onSetExceptionBreakpointsRequest(request)
|
||||
case *dap.ConfigurationDoneRequest:
|
||||
ds.onConfigurationDoneRequest(request)
|
||||
case *dap.ContinueRequest:
|
||||
ds.onContinueRequest(request)
|
||||
case *dap.NextRequest:
|
||||
ds.onNextRequest(request)
|
||||
case *dap.StepInRequest:
|
||||
ds.onStepInRequest(request)
|
||||
case *dap.StepOutRequest:
|
||||
ds.onStepOutRequest(request)
|
||||
case *dap.StepBackRequest:
|
||||
ds.onStepBackRequest(request)
|
||||
case *dap.ReverseContinueRequest:
|
||||
ds.onReverseContinueRequest(request)
|
||||
case *dap.RestartFrameRequest:
|
||||
ds.onRestartFrameRequest(request)
|
||||
case *dap.GotoRequest:
|
||||
ds.onGotoRequest(request)
|
||||
case *dap.PauseRequest:
|
||||
ds.onPauseRequest(request)
|
||||
case *dap.StackTraceRequest:
|
||||
ds.onStackTraceRequest(request)
|
||||
case *dap.ScopesRequest:
|
||||
ds.onScopesRequest(request)
|
||||
case *dap.VariablesRequest:
|
||||
ds.onVariablesRequest(request)
|
||||
case *dap.SetVariableRequest:
|
||||
ds.onSetVariableRequest(request)
|
||||
case *dap.SetExpressionRequest:
|
||||
ds.onSetExpressionRequest(request)
|
||||
case *dap.SourceRequest:
|
||||
ds.onSourceRequest(request)
|
||||
case *dap.ThreadsRequest:
|
||||
ds.onThreadsRequest(request)
|
||||
case *dap.TerminateThreadsRequest:
|
||||
ds.onTerminateThreadsRequest(request)
|
||||
case *dap.EvaluateRequest:
|
||||
ds.onEvaluateRequest(request)
|
||||
case *dap.StepInTargetsRequest:
|
||||
ds.onStepInTargetsRequest(request)
|
||||
case *dap.GotoTargetsRequest:
|
||||
ds.onGotoTargetsRequest(request)
|
||||
case *dap.CompletionsRequest:
|
||||
ds.onCompletionsRequest(request)
|
||||
case *dap.ExceptionInfoRequest:
|
||||
ds.onExceptionInfoRequest(request)
|
||||
case *dap.LoadedSourcesRequest:
|
||||
ds.onLoadedSourcesRequest(request)
|
||||
case *dap.DataBreakpointInfoRequest:
|
||||
ds.onDataBreakpointInfoRequest(request)
|
||||
case *dap.SetDataBreakpointsRequest:
|
||||
ds.onSetDataBreakpointsRequest(request)
|
||||
case *dap.ReadMemoryRequest:
|
||||
ds.onReadMemoryRequest(request)
|
||||
case *dap.DisassembleRequest:
|
||||
ds.onDisassembleRequest(request)
|
||||
case *dap.CancelRequest:
|
||||
ds.onCancelRequest(request)
|
||||
case *dap.BreakpointLocationsRequest:
|
||||
ds.onBreakpointLocationsRequest(request)
|
||||
default:
|
||||
log.Fatalf("Unable to process %#v", request)
|
||||
}
|
||||
}
|
||||
|
||||
// send lets the sender goroutine know via a channel that there is
|
||||
// a message to be sent to client. This is called by per-request
|
||||
// goroutines to send events and responses for each request and
|
||||
// to notify of events triggered by the fake debugger.
|
||||
func (ds *fakeDebugSession) send(message dap.Message) {
|
||||
ds.sendQueue <- message
|
||||
}
|
||||
|
||||
// sendFromQueue is to be run in a separate goroutine to listen on a
|
||||
// channel for messages to send back to the client. It will
|
||||
// return once the channel is closed.
|
||||
func (ds *fakeDebugSession) sendFromQueue() {
|
||||
for message := range ds.sendQueue {
|
||||
dap.WriteProtocolMessage(ds.rw.Writer, message)
|
||||
log.Printf("Message sent\n\t%#v\n", message)
|
||||
ds.rw.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Very Fake Debugger
|
||||
//
|
||||
|
||||
// The debugging session will keep track of how many breakpoints
|
||||
// have been set. Once start-up is done (i.e. configurationDone
|
||||
// request is processed), it will "stop" at each breakpoint one by
|
||||
// one, and once there are no more, it will trigger a terminated event.
|
||||
type fakeDebugSession struct {
|
||||
// rw is used to read requests and write events/responses
|
||||
rw *bufio.ReadWriter
|
||||
|
||||
// sendQueue is used to capture messages from multiple request
|
||||
// processing goroutines while writing them to the client connection
|
||||
// from a single goroutine via sendFromQueue. We must keep track of
|
||||
// the multiple channel senders with a wait group to make sure we do
|
||||
// not close this channel prematurely. Closing this channel will signal
|
||||
// the sendFromQueue goroutine that it can exit.
|
||||
sendQueue chan dap.Message
|
||||
sendWg sync.WaitGroup
|
||||
|
||||
// stopDebug is used to notify long-running handlers to stop processing.
|
||||
stopDebug chan struct{}
|
||||
|
||||
// bpSet is a counter of the remaining breakpoints that the debug
|
||||
// session is yet to stop at before the program terminates.
|
||||
bpSet int
|
||||
bpSetMux sync.Mutex
|
||||
}
|
||||
|
||||
// doContinue allows fake program execution to continue when the program
|
||||
// is started or unpaused. It simulates events from the debug session
|
||||
// by "stopping" on a breakpoint or terminating if there are no more
|
||||
// breakpoints. Safe to use concurrently.
|
||||
func (ds *fakeDebugSession) doContinue() {
|
||||
var e dap.Message
|
||||
ds.bpSetMux.Lock()
|
||||
if ds.bpSet == 0 {
|
||||
// Pretend that the program is running.
|
||||
// The delay will allow for all in-flight responses
|
||||
// to be sent before termination.
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
e = &dap.TerminatedEvent{
|
||||
Event: *newEvent("terminated"),
|
||||
}
|
||||
} else {
|
||||
e = &dap.StoppedEvent{
|
||||
Event: *newEvent("stopped"),
|
||||
Body: dap.StoppedEventBody{Reason: "breakpoint", ThreadId: 1, AllThreadsStopped: true},
|
||||
}
|
||||
ds.bpSet--
|
||||
}
|
||||
ds.bpSetMux.Unlock()
|
||||
ds.send(e)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Request Handlers
|
||||
//
|
||||
// Below is a dummy implementation of the request handlers.
|
||||
// They take no action, but just return dummy responses.
|
||||
// A real debug adaptor would call the debugger methods here
|
||||
// and use their results to populate each response.
|
||||
|
||||
func (ds *fakeDebugSession) onInitializeRequest(request *dap.InitializeRequest) {
|
||||
response := &dap.InitializeResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body.SupportsConfigurationDoneRequest = true
|
||||
response.Body.SupportsFunctionBreakpoints = false
|
||||
response.Body.SupportsConditionalBreakpoints = false
|
||||
response.Body.SupportsHitConditionalBreakpoints = false
|
||||
response.Body.SupportsEvaluateForHovers = false
|
||||
response.Body.ExceptionBreakpointFilters = []dap.ExceptionBreakpointsFilter{}
|
||||
response.Body.SupportsStepBack = false
|
||||
response.Body.SupportsSetVariable = false
|
||||
response.Body.SupportsRestartFrame = false
|
||||
response.Body.SupportsGotoTargetsRequest = false
|
||||
response.Body.SupportsStepInTargetsRequest = false
|
||||
response.Body.SupportsCompletionsRequest = false
|
||||
response.Body.CompletionTriggerCharacters = []string{}
|
||||
response.Body.SupportsModulesRequest = false
|
||||
response.Body.AdditionalModuleColumns = []dap.ColumnDescriptor{}
|
||||
response.Body.SupportedChecksumAlgorithms = []dap.ChecksumAlgorithm{}
|
||||
response.Body.SupportsRestartRequest = false
|
||||
response.Body.SupportsExceptionOptions = false
|
||||
response.Body.SupportsValueFormattingOptions = false
|
||||
response.Body.SupportsExceptionInfoRequest = false
|
||||
response.Body.SupportTerminateDebuggee = false
|
||||
response.Body.SupportsDelayedStackTraceLoading = false
|
||||
response.Body.SupportsLoadedSourcesRequest = false
|
||||
response.Body.SupportsLogPoints = false
|
||||
response.Body.SupportsTerminateThreadsRequest = false
|
||||
response.Body.SupportsSetExpression = false
|
||||
response.Body.SupportsTerminateRequest = false
|
||||
response.Body.SupportsDataBreakpoints = false
|
||||
response.Body.SupportsReadMemoryRequest = false
|
||||
response.Body.SupportsDisassembleRequest = false
|
||||
response.Body.SupportsCancelRequest = false
|
||||
response.Body.SupportsBreakpointLocationsRequest = false
|
||||
// This is a fake set up, so we can start "accepting" configuration
|
||||
// requests for setting breakpoints, etc from the client at any time.
|
||||
// Notify the client with an 'initialized' event. The client will end
|
||||
// the configuration sequence with 'configurationDone' request.
|
||||
e := &dap.InitializedEvent{Event: *newEvent("initialized")}
|
||||
ds.send(e)
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onLaunchRequest(request *dap.LaunchRequest) {
|
||||
// This is where a real debug adaptor would check the soundness of the
|
||||
// arguments (e.g. program from launch.json) and then use them to launch the
|
||||
// debugger and attach to the program.
|
||||
response := &dap.LaunchResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onAttachRequest(request *dap.AttachRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "AttachRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onDisconnectRequest(request *dap.DisconnectRequest) {
|
||||
response := &dap.DisconnectResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onTerminateRequest(request *dap.TerminateRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "TerminateRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onRestartRequest(request *dap.RestartRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "RestartRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
|
||||
response := &dap.SetBreakpointsResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body.Breakpoints = make([]dap.Breakpoint, len(request.Arguments.Breakpoints))
|
||||
for i, b := range request.Arguments.Breakpoints {
|
||||
response.Body.Breakpoints[i].Line = b.Line
|
||||
response.Body.Breakpoints[i].Verified = true
|
||||
ds.bpSetMux.Lock()
|
||||
ds.bpSet++
|
||||
ds.bpSetMux.Unlock()
|
||||
}
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpointsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "SetFunctionBreakpointsRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
|
||||
response := &dap.SetExceptionBreakpointsResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest) {
|
||||
// This would be the place to check if the session was configured to
|
||||
// stop on entry and if that is the case, to issue a
|
||||
// stopped-on-breakpoint event. This being a mock implementation,
|
||||
// we "let" the program continue after sending a successful response.
|
||||
e := &dap.ThreadEvent{Event: *newEvent("thread"), Body: dap.ThreadEventBody{Reason: "started", ThreadId: 1}}
|
||||
ds.send(e)
|
||||
response := &dap.ConfigurationDoneResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
ds.send(response)
|
||||
ds.doContinue()
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onContinueRequest(request *dap.ContinueRequest) {
|
||||
response := &dap.ContinueResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
ds.send(response)
|
||||
ds.doContinue()
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onNextRequest(request *dap.NextRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "NextRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onStepInRequest(request *dap.StepInRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "StepInRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onStepOutRequest(request *dap.StepOutRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "StepOutRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onStepBackRequest(request *dap.StepBackRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "StepBackRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onReverseContinueRequest(request *dap.ReverseContinueRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "ReverseContinueRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onRestartFrameRequest(request *dap.RestartFrameRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "RestartFrameRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onGotoRequest(request *dap.GotoRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "GotoRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onPauseRequest(request *dap.PauseRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "PauseRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onStackTraceRequest(request *dap.StackTraceRequest) {
|
||||
response := &dap.StackTraceResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body = dap.StackTraceResponseBody{
|
||||
StackFrames: []dap.StackFrame{
|
||||
{
|
||||
Id: 1000,
|
||||
Source: &dap.Source{Name: "hello.go", Path: "/Users/foo/go/src/hello/hello.go", SourceReference: 0},
|
||||
Line: 5,
|
||||
Column: 0,
|
||||
Name: "main.main",
|
||||
},
|
||||
},
|
||||
TotalFrames: 1,
|
||||
}
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onScopesRequest(request *dap.ScopesRequest) {
|
||||
response := &dap.ScopesResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body = dap.ScopesResponseBody{
|
||||
Scopes: []dap.Scope{
|
||||
{Name: "Local", VariablesReference: 1000, Expensive: false},
|
||||
{Name: "Global", VariablesReference: 1001, Expensive: true},
|
||||
},
|
||||
}
|
||||
ds.send(response)
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onVariablesRequest(request *dap.VariablesRequest) {
|
||||
select {
|
||||
case <-ds.stopDebug:
|
||||
return
|
||||
// simulate long-running processing to make this handler
|
||||
// respond to this request after the next request is received
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
response := &dap.VariablesResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body = dap.VariablesResponseBody{
|
||||
Variables: []dap.Variable{{Name: "i", Value: "18434528", EvaluateName: "i", VariablesReference: 0}},
|
||||
}
|
||||
ds.send(response)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetVariableRequest(request *dap.SetVariableRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "setVariableRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetExpressionRequest(request *dap.SetExpressionRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "SetExpressionRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSourceRequest(request *dap.SourceRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "SourceRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onThreadsRequest(request *dap.ThreadsRequest) {
|
||||
response := &dap.ThreadsResponse{}
|
||||
response.Response = *newResponse(request.Seq, request.Command)
|
||||
response.Body = dap.ThreadsResponseBody{Threads: []dap.Thread{{Id: 1, Name: "main"}}}
|
||||
ds.send(response)
|
||||
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onTerminateThreadsRequest(request *dap.TerminateThreadsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "TerminateRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onEvaluateRequest(request *dap.EvaluateRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "EvaluateRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onStepInTargetsRequest(request *dap.StepInTargetsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "StepInTargetRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onGotoTargetsRequest(request *dap.GotoTargetsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "GotoTargetRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onCompletionsRequest(request *dap.CompletionsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "CompletionRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "ExceptionRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "LoadedRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onDataBreakpointInfoRequest(request *dap.DataBreakpointInfoRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "DataBreakpointInfoRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onSetDataBreakpointsRequest(request *dap.SetDataBreakpointsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "SetDataBreakpointsRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "ReadMemoryRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onDisassembleRequest(request *dap.DisassembleRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "DisassembleRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onCancelRequest(request *dap.CancelRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "CancelRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func (ds *fakeDebugSession) onBreakpointLocationsRequest(request *dap.BreakpointLocationsRequest) {
|
||||
ds.send(newErrorResponse(request.Seq, request.Command, "BreakpointLocationsRequest is not yet supported"))
|
||||
}
|
||||
|
||||
func newEvent(event string) *dap.Event {
|
||||
return &dap.Event{
|
||||
ProtocolMessage: dap.ProtocolMessage{
|
||||
Seq: 0,
|
||||
Type: "event",
|
||||
},
|
||||
Event: event,
|
||||
}
|
||||
}
|
||||
|
||||
func newResponse(requestSeq int, command string) *dap.Response {
|
||||
return &dap.Response{
|
||||
ProtocolMessage: dap.ProtocolMessage{
|
||||
Seq: 0,
|
||||
Type: "response",
|
||||
},
|
||||
Command: command,
|
||||
RequestSeq: requestSeq,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newErrorResponse(requestSeq int, command string, message string) *dap.ErrorResponse {
|
||||
er := &dap.ErrorResponse{}
|
||||
er.Response = *newResponse(requestSeq, command)
|
||||
er.Success = false
|
||||
er.Message = "unsupported"
|
||||
er.Body.Error.Format = message
|
||||
er.Body.Error.Id = 12345
|
||||
return er
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
)
|
||||
|
||||
var initializeRequest = []byte(`{"seq":1,"type":"request","command":"initialize","arguments":{"clientID":"vscode","clientName":"Visual Studio Code","adapterID":"go","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"}}`)
|
||||
var initializedEvent = []byte(`{"seq":0,"type":"event","event":"initialized"}`)
|
||||
var initializeResponse = []byte(`{"seq":0,"type":"response","request_seq":1,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true}}`)
|
||||
|
||||
var launchRequest = []byte(`{"seq":2,"type":"request","command":"launch","arguments":{"noDebug": true,"name":"Launch","type":"go","request":"launch","mode":"debug","program":"/Users/foo/go/src/hello","__sessionId":"4c88179f-1202-4f75-9e67-5bf535cde30a","args":["somearg"],"env":{"GOPATH":"/Users/foo/go","HOME":"/Users/foo","SHELL":"/bin/bash"}}}`)
|
||||
var launchResponse = []byte(`{"seq":0,"type":"response","request_seq":2,"success":true,"command":"launch"}`)
|
||||
|
||||
var setBreakpointsRequest = []byte(`{"seq":3,"type":"request","command":"setBreakpoints","arguments":{"source":{"name":"hello.go","path":"/Users/foo/go/src/hello/hello.go"},"lines":[7],"breakpoints":[{"line":7}],"sourceModified":false}}`)
|
||||
var setBreakpointsResponse = []byte(`{"seq":0,"type":"response","request_seq":3,"success":true,"command":"setBreakpoints","body":{"breakpoints":[{"verified":true,"line":7}]}}`)
|
||||
|
||||
var setExceptionBreakpointsRequest = []byte(`{"seq":4,"type":"request","command":"setExceptionBreakpoints","arguments":{"filters":[]}}`)
|
||||
var setExceptionBreakpointsResponse = []byte(`{"seq":0,"type":"response","request_seq":4,"success":true,"command":"setExceptionBreakpoints","body":{}}`)
|
||||
|
||||
var configurationDoneRequest = []byte(`{"seq":5,"type":"request","command":"configurationDone"}`)
|
||||
var threadEvent = []byte(`{"seq":0,"type":"event","event":"thread","body":{"reason":"started","threadId":1}}`)
|
||||
var configurationDoneResponse = []byte(`{"seq":0,"type":"response","request_seq":5,"success":true,"command":"configurationDone"}`)
|
||||
|
||||
var stoppedEvent = []byte(`{"seq":0,"type":"event","event":"stopped","body":{"reason":"breakpoint","threadId":1,"allThreadsStopped":true}}`)
|
||||
|
||||
var threadsRequest = []byte(`{"seq":6,"type":"request","command":"threads"}`)
|
||||
var threadsResponse = []byte(`{"seq":0,"type":"response","request_seq":6,"success":true,"command":"threads","body":{"threads":[{"id":1,"name":"main"}]}}`)
|
||||
|
||||
var stackTraceRequest = []byte(`{"seq":7,"type":"request","command":"stackTrace","arguments":{"threadId":1,"startFrame":0,"levels":20}}`)
|
||||
var stackTraceResponse = []byte(`{"seq":0,"type":"response","request_seq":7,"success":true,"command":"stackTrace","body":{"stackFrames":[{"id":1000,"name":"main.main","source":{"name":"hello.go","path":"/Users/foo/go/src/hello/hello.go"},"line":5,"column":0}],"totalFrames":1}}`)
|
||||
|
||||
var scopesRequest = []byte(`{"seq":8,"type":"request","command":"scopes","arguments":{"frameId":1000}}`)
|
||||
var scopesResponse = []byte(`{"seq":0,"type":"response","request_seq":8,"success":true,"command":"scopes","body":{"scopes":[{"name":"Local","variablesReference":1000,"expensive":false},{"name":"Global","variablesReference":1001,"expensive":true}]}}`)
|
||||
|
||||
var variablesRequest = []byte(`{"seq":9,"type":"request","command":"variables","arguments":{"variablesReference":1000}}`)
|
||||
var variablesResponse = []byte(`{"seq":0,"type":"response","request_seq":9,"success":true,"command":"variables","body":{"variables":[{"name":"i","value":"18434528","evaluateName":"i","variablesReference":0}]}}`)
|
||||
|
||||
var continueRequest = []byte(`{"seq":10,"type":"request","command":"continue","arguments":{"threadId":1}}`)
|
||||
var continueResponse = []byte(`{"seq":0,"type":"response","request_seq":10,"success":true,"command":"continue","body":{"allThreadsContinued":false}}`)
|
||||
|
||||
var terminatedEvent = []byte(`{"seq":0,"type":"event","event":"terminated","body":{}}`)
|
||||
var disconnectRequest = []byte(`{"seq":11,"type":"request","command":"disconnect","arguments":{"restart":false}}`)
|
||||
var disconnectResponse = []byte(`{"seq":0,"type":"response","request_seq":11,"success":true,"command":"disconnect"}`)
|
||||
|
||||
func expectMessage(t *testing.T, r *bufio.Reader, want []byte) {
|
||||
got, err := dap.ReadBaseMessage(r)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("\ngot %q\nwant %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
port := "54321"
|
||||
go func() {
|
||||
err := server(port)
|
||||
if err != nil {
|
||||
log.Fatal("Could not start server:", err)
|
||||
}
|
||||
}()
|
||||
// Give server time to start listening before clients connect
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go client(t, port, &wg)
|
||||
go client(t, port, &wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func client(t *testing.T, port string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
conn, err := net.Dial("tcp", ":"+port)
|
||||
if err != nil {
|
||||
log.Fatal("Could not connect to server:", err)
|
||||
}
|
||||
defer func() {
|
||||
t.Log("Closing connection to server at", conn.RemoteAddr())
|
||||
conn.Close()
|
||||
}()
|
||||
t.Log("Connected to server at", conn.RemoteAddr())
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
|
||||
// Start up
|
||||
|
||||
dap.WriteBaseMessage(conn, initializeRequest)
|
||||
expectMessage(t, r, initializedEvent)
|
||||
expectMessage(t, r, initializeResponse)
|
||||
|
||||
dap.WriteBaseMessage(conn, launchRequest)
|
||||
expectMessage(t, r, launchResponse)
|
||||
|
||||
dap.WriteBaseMessage(conn, setBreakpointsRequest)
|
||||
expectMessage(t, r, setBreakpointsResponse)
|
||||
dap.WriteBaseMessage(conn, setExceptionBreakpointsRequest)
|
||||
expectMessage(t, r, setExceptionBreakpointsResponse)
|
||||
|
||||
dap.WriteBaseMessage(conn, configurationDoneRequest)
|
||||
expectMessage(t, r, threadEvent)
|
||||
expectMessage(t, r, configurationDoneResponse)
|
||||
|
||||
// Stop on preconfigured breakpoint & Continue
|
||||
|
||||
expectMessage(t, r, stoppedEvent)
|
||||
|
||||
dap.WriteBaseMessage(conn, threadsRequest)
|
||||
expectMessage(t, r, threadsResponse)
|
||||
|
||||
dap.WriteBaseMessage(conn, stackTraceRequest)
|
||||
expectMessage(t, r, stackTraceResponse)
|
||||
|
||||
dap.WriteBaseMessage(conn, scopesRequest)
|
||||
expectMessage(t, r, scopesResponse)
|
||||
|
||||
// Processing of this request will be slow due to a fake delay.
|
||||
// Send the next request right away and confirm that processing
|
||||
// happens concurrently and the two responses are received
|
||||
// out of order.
|
||||
dap.WriteBaseMessage(conn, variablesRequest)
|
||||
dap.WriteBaseMessage(conn, continueRequest)
|
||||
expectMessage(t, r, continueResponse)
|
||||
expectMessage(t, r, variablesResponse)
|
||||
|
||||
// Shut down
|
||||
|
||||
expectMessage(t, r, terminatedEvent)
|
||||
dap.WriteBaseMessage(conn, disconnectRequest)
|
||||
expectMessage(t, r, disconnectResponse)
|
||||
}
|
||||
Reference in New Issue
Block a user