whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go-version: [1.20.x]
|
||||
include:
|
||||
- go-version: 1.19.x
|
||||
os: ubuntu-latest
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: 'internal/test.sh'
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
||||
@@ -0,0 +1,20 @@
|
||||
# go-dap: Go implementation of the Debug Adapter Protocol
|
||||
|
||||
[](https://pkg.go.dev/github.com/google/go-dap)
|
||||
[](https://github.com/google/go-dap/actions)
|
||||
[](https://goreportcard.com/report/github.com/google/go-dap)
|
||||
|
||||
For an overview of DAP, see
|
||||
https://microsoft.github.io/debug-adapter-protocol/overview
|
||||
|
||||
## Contributing
|
||||
|
||||
We'd love to accept your patches and contributions to this project. See
|
||||
[docs/contributing](https://github.com/google/go-dap/blob/master/docs/contributing.md)
|
||||
for more details.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License 2.0
|
||||
|
||||
This is not an officially supported Google product.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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 contains utilities for decoding JSON-encoded bytes into DAP message.
|
||||
|
||||
package dap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DecodeProtocolMessageFieldError describes which JSON attribute
|
||||
// has an unsupported value that the decoding cannot handle.
|
||||
type DecodeProtocolMessageFieldError struct {
|
||||
Seq int
|
||||
SubType string
|
||||
FieldName string
|
||||
FieldValue string
|
||||
Message json.RawMessage
|
||||
}
|
||||
|
||||
func (e *DecodeProtocolMessageFieldError) Error() string {
|
||||
return fmt.Sprintf("%s %s '%s' is not supported (seq: %d)", e.SubType, e.FieldName, e.FieldValue, e.Seq)
|
||||
}
|
||||
|
||||
// defaultCodec is used to decode vanilla DAP messages.
|
||||
var defaultCodec = NewCodec()
|
||||
|
||||
// Codec is responsible for turning byte blobs into DAP messages.
|
||||
type Codec struct {
|
||||
eventCtor map[string]messageCtor
|
||||
requestCtor map[string]messageCtor
|
||||
responseCtor map[string]messageCtor
|
||||
}
|
||||
|
||||
// NewCodec constructs a new codec that extends the vanilla DAP protocol.
|
||||
// Unless you need to register custom DAP messages, use
|
||||
// DecodeProtocolMessage instead.
|
||||
func NewCodec() *Codec {
|
||||
ret := &Codec{
|
||||
eventCtor: make(map[string]messageCtor),
|
||||
requestCtor: make(map[string]messageCtor),
|
||||
responseCtor: make(map[string]messageCtor),
|
||||
}
|
||||
for k, v := range eventCtor {
|
||||
ret.eventCtor[k] = v
|
||||
}
|
||||
for k, v := range requestCtor {
|
||||
ret.requestCtor[k] = v
|
||||
}
|
||||
for k, v := range responseCtor {
|
||||
ret.responseCtor[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// RegisterRequest registers a new custom DAP command, so that it can be
|
||||
// unmarshalled by DecodeMessage. Returns an error when the command already
|
||||
// exists.
|
||||
//
|
||||
// The ctor functions need to return a new instance of the underlying DAP
|
||||
// message type. A typical usage looks like this:
|
||||
//
|
||||
// reqCtor := func() Message { return &LaunchRequest{} }
|
||||
// respCtor := func() Message { return &LaunchResponse{} }
|
||||
// codec.RegisterRequest("launch", reqCtor, respCtor)
|
||||
func (c *Codec) RegisterRequest(command string, requestCtor, responseCtor func() Message) error {
|
||||
_, hasReqCtor := c.requestCtor[command]
|
||||
_, hasRespCtor := c.responseCtor[command]
|
||||
if hasReqCtor || hasRespCtor {
|
||||
return fmt.Errorf("command %q is already registered", command)
|
||||
}
|
||||
c.requestCtor[command] = requestCtor
|
||||
c.responseCtor[command] = responseCtor
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterEvent registers a new custom DAP event, so that it can be
|
||||
// unmarshalled by DecodeMessage. Returns an error when the event already
|
||||
// exists.
|
||||
//
|
||||
// The ctor function needs to return a new instance of the underlying DAP
|
||||
// message type. A typical usage looks like this:
|
||||
//
|
||||
// ctor := func() Message { return &StoppedEvent{} }
|
||||
// codec.RegisterEvent("stopped", ctor)
|
||||
func (c *Codec) RegisterEvent(event string, ctor func() Message) error {
|
||||
if _, hasEventCtor := c.eventCtor[event]; hasEventCtor {
|
||||
return fmt.Errorf("event %q is already registered", event)
|
||||
}
|
||||
c.eventCtor[event] = ctor
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeMessage parses the JSON-encoded data and returns the result of
|
||||
// the appropriate type within the ProtocolMessage hierarchy. If message type,
|
||||
// command, etc cannot be cast, returns DecodeProtocolMessageFieldError.
|
||||
// See also godoc for json.Unmarshal, which is used for underlying decoding.
|
||||
func (c *Codec) DecodeMessage(data []byte) (Message, error) {
|
||||
// This struct is the union of the ResponseMessage, RequestMessage, and
|
||||
// EventMessage types. It is an optimization that saves an additional
|
||||
// json.Unmarshal call.
|
||||
var m struct {
|
||||
ProtocolMessage
|
||||
Command string `json:"command"`
|
||||
Event string `json:"event"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch m.Type {
|
||||
case "request":
|
||||
return c.decodeRequest(m.Command, m.Seq, data)
|
||||
case "response":
|
||||
return c.decodeResponse(m.Command, m.Seq, m.Success, data)
|
||||
case "event":
|
||||
return c.decodeEvent(m.Event, m.Seq, data)
|
||||
default:
|
||||
return nil, &DecodeProtocolMessageFieldError{m.Seq, "ProtocolMessage", "type", m.Type, json.RawMessage(data)}
|
||||
}
|
||||
}
|
||||
|
||||
// decodeRequest determines what request type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeRequest(command string, seq int, data []byte) (Message, error) {
|
||||
ctor, ok := c.requestCtor[command]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Request", "command", command, json.RawMessage(data)}
|
||||
}
|
||||
requestPtr := ctor()
|
||||
err := json.Unmarshal(data, requestPtr)
|
||||
return requestPtr, err
|
||||
}
|
||||
|
||||
// decodeResponse determines what response type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeResponse(command string, seq int, success bool, data []byte) (Message, error) {
|
||||
if !success {
|
||||
var er ErrorResponse
|
||||
err := json.Unmarshal(data, &er)
|
||||
return &er, err
|
||||
}
|
||||
ctor, ok := c.responseCtor[command]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Response", "command", command, json.RawMessage(data)}
|
||||
}
|
||||
responsePtr := ctor()
|
||||
err := json.Unmarshal(data, responsePtr)
|
||||
return responsePtr, err
|
||||
}
|
||||
|
||||
// decodeEvent determines what event type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeEvent(event string, seq int, data []byte) (Message, error) {
|
||||
ctor, ok := c.eventCtor[event]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Event", "event", event, json.RawMessage(data)}
|
||||
}
|
||||
eventPtr := ctor()
|
||||
err := json.Unmarshal(data, eventPtr)
|
||||
return eventPtr, err
|
||||
}
|
||||
|
||||
// DecodeProtocolMessage parses the JSON-encoded ProtocolMessage and returns
|
||||
// the message embedded in it. If message type, command, etc cannot be cast,
|
||||
// returns DecodeProtocolMessageFieldError. See also godoc for json.Unmarshal,
|
||||
// which is used for underlying decoding.
|
||||
func DecodeProtocolMessage(data []byte) (Message, error) {
|
||||
return defaultCodec.DecodeMessage(data)
|
||||
}
|
||||
|
||||
type messageCtor func() Message
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
// Copyright 2021 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 dap contains data types and code for Debug Adapter Protocol (DAP) specification.
|
||||
// https://github.com/microsoft/vscode-debugadapter-node/blob/main/debugProtocol.json
|
||||
package dap
|
||||
|
||||
//go:generate go run ./cmd/gentypes/gentypes.go -o schematypes.go -u cmd/gentypes/debugProtocol.json
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# How to Contribute
|
||||
|
||||
We'd love to accept your patches and contributions to this project. There are
|
||||
just a few small guidelines you need to follow.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution;
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## Code reviews
|
||||
|
||||
All submissions, including submissions by project members, require review. We
|
||||
use GitHub pull requests for this purpose. Consult
|
||||
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
|
||||
information on using pull requests.
|
||||
|
||||
## Source Code Headers
|
||||
|
||||
Every file containing source code must include copyright and license
|
||||
information. This includes any JS/CSS files that you might be serving out to
|
||||
browsers. (This is to help well-intentioned people avoid accidental copying that
|
||||
doesn't comply with the license.)
|
||||
|
||||
Apache header:
|
||||
|
||||
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.
|
||||
|
||||
## Community Guidelines
|
||||
|
||||
This project follows [Google's Open Source Community
|
||||
Guidelines](https://opensource.google/conduct/).
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/google/go-dap
|
||||
|
||||
go 1.18
|
||||
|
||||
retract v0.9.0
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
echo "usage: runchecks.sh" 1>&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
# ensure_go_binary verifies that a binary exists in $PATH corresponding to the
|
||||
# given go-gettable URI. If no such binary exists, it is fetched via `go install`
|
||||
# at latest.
|
||||
ensure_go_binary() {
|
||||
local binary=$(basename $1)
|
||||
if ! [ -x "$(command -v $binary)" ]; then
|
||||
echo "Installing: $1"
|
||||
# Run in a subshell for convenience, so that we don't have to worry about
|
||||
# our PWD.
|
||||
(set -x; cd && go install $1@latest)
|
||||
fi
|
||||
}
|
||||
|
||||
echo "**** Running Go tests"
|
||||
go test -race -count=1 ./...
|
||||
|
||||
echo "**** Running staticcheck"
|
||||
ensure_go_binary honnef.co/go/tools/cmd/staticcheck
|
||||
staticcheck ./...
|
||||
|
||||
echo "**** Running go vet"
|
||||
go vet ./...
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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 contains utilities for DAP Base protocol I/O.
|
||||
// For additional information, see "Base protocol" section in
|
||||
// https://microsoft.github.io/debug-adapter-protocol/overview.
|
||||
|
||||
package dap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BaseProtocolError represents base protocol error, which occurs when the raw
|
||||
// message does not conform to the header+content format of the base protocol.
|
||||
type BaseProtocolError struct {
|
||||
Err string
|
||||
}
|
||||
|
||||
func (bpe *BaseProtocolError) Error() string { return bpe.Err }
|
||||
|
||||
var (
|
||||
// ErrHeaderDelimiterNotCrLfCrLf is returned when only partial header
|
||||
// delimiter \r\n\r\n is encountered.
|
||||
ErrHeaderDelimiterNotCrLfCrLf = &BaseProtocolError{fmt.Sprintf("header delimiter is not %q", crLfcrLf)}
|
||||
|
||||
// ErrHeaderNotContentLength is returned when the parsed header is
|
||||
// not of valid Content-Length format.
|
||||
ErrHeaderNotContentLength = &BaseProtocolError{fmt.Sprintf("header format is not %q", contentLengthHeaderRegex)}
|
||||
|
||||
// ErrHeaderContentTooLong is returned when the content length specified in
|
||||
// the header is above contentMaxLength.
|
||||
ErrHeaderContentTooLong = &BaseProtocolError{fmt.Sprintf("content length over %v bytes", contentMaxLength)}
|
||||
)
|
||||
|
||||
const (
|
||||
crLfcrLf = "\r\n\r\n"
|
||||
contentLengthHeaderFmt = "Content-Length: %d\r\n\r\n"
|
||||
contentMaxLength = 4 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
contentLengthHeaderRegex = regexp.MustCompile("^Content-Length: ([0-9]+)$")
|
||||
)
|
||||
|
||||
// WriteBaseMessage formats content with Content-Length header and delimiters
|
||||
// as per the base protocol and writes the resulting message to w.
|
||||
func WriteBaseMessage(w io.Writer, content []byte) error {
|
||||
header := fmt.Sprintf(contentLengthHeaderFmt, len(content))
|
||||
if _, err := w.Write([]byte(header)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(content)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadBaseMessage reads one message from r consisting of a Content-Length
|
||||
// header and a content part. It parses the header to determine the size of
|
||||
// the content part and extracts and returns the actual content of the message.
|
||||
// Returns nil bytes on error, which can be one of the standard IO errors or
|
||||
// a BaseProtocolError defined in this package.
|
||||
func ReadBaseMessage(r *bufio.Reader) ([]byte, error) {
|
||||
contentLength, err := readContentLengthHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentLength > contentMaxLength {
|
||||
return nil, ErrHeaderContentTooLong
|
||||
}
|
||||
content := make([]byte, contentLength)
|
||||
if _, err = io.ReadFull(r, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// readContentLengthHeader looks for the only header field that is supported
|
||||
// and required:
|
||||
// Content-Length: [0-9]+\r\n\r\n
|
||||
// Extracts and returns the content length.
|
||||
func readContentLengthHeader(r *bufio.Reader) (contentLength int64, err error) {
|
||||
// Look for <some header>\r\n\r\n
|
||||
headerWithCr, err := r.ReadString('\r')
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nextThree := make([]byte, 3)
|
||||
if _, err = io.ReadFull(r, nextThree); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if string(nextThree) != "\n\r\n" {
|
||||
return 0, ErrHeaderDelimiterNotCrLfCrLf
|
||||
}
|
||||
|
||||
// If header is in the right format, get the length
|
||||
header := strings.TrimSuffix(headerWithCr, "\r")
|
||||
headerAndLength := contentLengthHeaderRegex.FindStringSubmatch(header)
|
||||
if len(headerAndLength) < 2 {
|
||||
return 0, ErrHeaderNotContentLength
|
||||
}
|
||||
return strconv.ParseInt(headerAndLength[1], 10, 64)
|
||||
}
|
||||
|
||||
// WriteProtocolMessage encodes message and writes it to w.
|
||||
func WriteProtocolMessage(w io.Writer, message Message) error {
|
||||
b, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteBaseMessage(w, b)
|
||||
}
|
||||
|
||||
// ReadProtocolMessage reads a message from r, decodes and returns it.
|
||||
func ReadProtocolMessage(r *bufio.Reader) (Message, error) {
|
||||
content, err := ReadBaseMessage(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DecodeProtocolMessage(content)
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// 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 dap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_WriteBaseMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantWritten string
|
||||
wantErr error
|
||||
}{
|
||||
{``, "Content-Length: 0\r\n\r\n", nil},
|
||||
{`a`, "Content-Length: 1\r\n\r\na", nil},
|
||||
{`{}`, "Content-Length: 2\r\n\r\n{}", nil},
|
||||
{`{"a":0 "b":"blah"}`, "Content-Length: 18\r\n\r\n{\"a\":0 \"b\":\"blah\"}", nil},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
gotErr := WriteBaseMessage(&buf, []byte(test.input))
|
||||
gotWritten := buf.String()
|
||||
if gotErr != test.wantErr {
|
||||
t.Errorf("got err=%#v, want %#v", gotErr, test.wantErr)
|
||||
}
|
||||
if gotErr == nil && gotWritten != test.wantWritten {
|
||||
t.Errorf("got written=%q, want %q", gotWritten, test.wantWritten)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ReadBaseMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantBytesRead []byte
|
||||
wantBytesLeft []byte
|
||||
wantErr error
|
||||
}{
|
||||
{"", nil, []byte(""), io.EOF},
|
||||
{"random stuff\r\nabc", nil, []byte("c"), ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Cache-Control: no-cache\r\n\r\n", nil, []byte(""), ErrHeaderNotContentLength},
|
||||
{"Content-Length 1\r\n\r\nabc", nil, []byte("abc"), ErrHeaderNotContentLength},
|
||||
{"Content-Length: 10\r\n\r\nabc", nil, []byte(""), io.ErrUnexpectedEOF},
|
||||
{"Content-Length: 3\r\n\r\nabc", []byte("abc"), []byte(""), nil},
|
||||
{"Content-Length: 4194305\r\n\r\nabc", nil, []byte("abc"), ErrHeaderContentTooLong},
|
||||
{"Content-Length: 6506440440440\r\n\r\nabc", nil, []byte("abc"), ErrHeaderContentTooLong},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
reader := bufio.NewReader(strings.NewReader(test.input))
|
||||
gotBytes, gotErr := ReadBaseMessage(reader)
|
||||
if gotErr != test.wantErr {
|
||||
t.Errorf("got err=%#v, want %#v", gotErr, test.wantErr)
|
||||
}
|
||||
if gotErr == nil && !bytes.Equal(gotBytes, test.wantBytesRead) {
|
||||
t.Errorf("got bytes=%q, want %q", gotBytes, test.wantBytesRead)
|
||||
}
|
||||
bytesLeft, _ := ioutil.ReadAll(reader)
|
||||
if !bytes.Equal(bytesLeft, test.wantBytesLeft) {
|
||||
t.Errorf("got bytesLeft=%q, want %q", bytesLeft, test.wantBytesLeft)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_readContentLengthHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantBytesLeft string // Bytes left in the reader after header reading
|
||||
wantLen int64 // Extracted content length value
|
||||
wantErr error
|
||||
}{
|
||||
{"", "", 0, io.EOF},
|
||||
{"Cache-Control: no-cache", "", 0, io.EOF},
|
||||
{"Cache-Control: no-cache\r", "", 0, io.EOF},
|
||||
{"Cache-Control: no-cache\rabc", "", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Cache-Control: no-cache\r\n", "", 0, io.ErrUnexpectedEOF},
|
||||
{"Cache-Control: no-cache\r\n\r", "", 0, io.ErrUnexpectedEOF},
|
||||
{"Cache-Control: no-cache\r\n\r\n", "", 0, ErrHeaderNotContentLength},
|
||||
{"Cache-Control: no-cache\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: 3 abc", "", 0, io.EOF},
|
||||
{"Content-Length: 3\nabc", "", 0, io.EOF},
|
||||
{"Content-Length: 3\rabc", "", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length: 3\r\nabc", "c", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length: 3\r\n\rabc", "bc", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length: 3\r \n\r\nabc", "\nabc", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length: 3\r\n \r\nabc", "\nabc", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length: 3\r\n\r \nabc", "\nabc", 0, ErrHeaderDelimiterNotCrLfCrLf},
|
||||
{"Content-Length 3\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"_Content-Length: 3\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: 3_\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: x\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: 3.0\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: -3\r\n\r\nabc", "abc", 0, ErrHeaderNotContentLength},
|
||||
{"Content-Length: 0\r\n\r\nabc", "abc", 0, nil},
|
||||
{"Content-Length: 3\r\n\r\nabc", "abc", 3, nil},
|
||||
{"Content-Length: 9223372036854775807\r\n\r\nabc", "abc", 9223372036854775807, nil},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
reader := bufio.NewReader(strings.NewReader(test.input))
|
||||
gotLen, gotErr := readContentLengthHeader(reader)
|
||||
if gotErr != test.wantErr {
|
||||
t.Errorf("got err=%#v, want %#v", gotErr, test.wantErr)
|
||||
}
|
||||
if gotErr == nil && gotLen != test.wantLen {
|
||||
t.Errorf("got len=%d, want %d", gotLen, test.wantLen)
|
||||
}
|
||||
bytesLeft, _ := ioutil.ReadAll(reader)
|
||||
if string(bytesLeft) != test.wantBytesLeft {
|
||||
t.Errorf("got bytesLeft=%q, want %q", bytesLeft, test.wantBytesLeft)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRead(t *testing.T) {
|
||||
writeContent := [][]byte{
|
||||
[]byte("this is"),
|
||||
[]byte("a read write"),
|
||||
[]byte("test"),
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, wc := range writeContent {
|
||||
if err := WriteBaseMessage(&buf, wc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
reader := bufio.NewReader(&buf)
|
||||
for _, wc := range writeContent {
|
||||
rc, err := ReadBaseMessage(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(rc, wc) {
|
||||
t.Fatalf("got %q, want %q", rc, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readMessagesAndNotify reads messages one by one until EOF.
|
||||
// Notifies of a read via messagesRead channel.
|
||||
func readMessagesAndNotify(t *testing.T, r io.Reader, messagesRead chan<- []byte) {
|
||||
reader := bufio.NewReader(r)
|
||||
for {
|
||||
msg, err := ReadBaseMessage(reader)
|
||||
if err == io.EOF {
|
||||
close(messagesRead)
|
||||
break
|
||||
}
|
||||
// On error, this will send "" as the content read
|
||||
messagesRead <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func writeOrFail(t *testing.T, w io.Writer, data string) {
|
||||
if n, err := w.Write([]byte(data)); err != nil || n < len(data) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMessageInParts(t *testing.T) {
|
||||
// This test uses separate goroutines to write and read messages
|
||||
// and relies on blocking channel operations between them to ensure that
|
||||
// the expected number of messages is read for what is written.
|
||||
// Otherwise, the test will time out.
|
||||
messagesRead := make(chan []byte)
|
||||
r, w := io.Pipe()
|
||||
header := "Content-Length: 11"
|
||||
baddelim := "\r\r\r\r"
|
||||
content1 := "message one"
|
||||
content2 := "message two"
|
||||
nocontent := ""
|
||||
|
||||
// This will keep blocking to read a full message or EOF.
|
||||
go readMessagesAndNotify(t, r, messagesRead)
|
||||
|
||||
checkNoMessageRead := func() {
|
||||
time.Sleep(100 * time.Millisecond) // Let read goroutine run
|
||||
select {
|
||||
case msg := <-messagesRead:
|
||||
t.Errorf("got %q, want none", msg)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
checkOneMessageRead := func(want []byte) {
|
||||
got := <-messagesRead
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Good message written in full
|
||||
writeOrFail(t, w, header+crLfcrLf+content1)
|
||||
checkOneMessageRead([]byte(content1))
|
||||
|
||||
// Good message written in parts
|
||||
writeOrFail(t, w, header)
|
||||
checkNoMessageRead()
|
||||
writeOrFail(t, w, crLfcrLf)
|
||||
checkNoMessageRead()
|
||||
writeOrFail(t, w, content2)
|
||||
checkOneMessageRead([]byte(content2))
|
||||
|
||||
// Bad message written in full
|
||||
writeOrFail(t, w, header+baddelim)
|
||||
checkOneMessageRead([]byte(nocontent))
|
||||
|
||||
// Bad meassage written in parts
|
||||
writeOrFail(t, w, header)
|
||||
checkNoMessageRead()
|
||||
writeOrFail(t, w, baddelim)
|
||||
checkOneMessageRead([]byte(nocontent))
|
||||
|
||||
w.Close() // "sends" EOF
|
||||
}
|
||||
|
||||
func TestReadWriteWithCodec(t *testing.T) {
|
||||
// Tests end-to-end write and read from a buffer using the DAP codec.
|
||||
req := InitializeRequest{
|
||||
Request: Request{
|
||||
ProtocolMessage: ProtocolMessage{
|
||||
Type: "request",
|
||||
Seq: 121,
|
||||
},
|
||||
Command: "initialize",
|
||||
},
|
||||
Arguments: InitializeRequestArguments{
|
||||
ClientID: "vscode",
|
||||
ClientName: "Visual Studio Code",
|
||||
AdapterID: "go",
|
||||
PathFormat: "path",
|
||||
LinesStartAt1: true,
|
||||
ColumnsStartAt1: false,
|
||||
SupportsVariableType: true,
|
||||
SupportsVariablePaging: true,
|
||||
SupportsRunInTerminalRequest: false,
|
||||
Locale: "en-us",
|
||||
},
|
||||
}
|
||||
|
||||
baseReq, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err = WriteBaseMessage(buf, baseReq)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(buf)
|
||||
msg, err := ReadBaseMessage(reader)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
readReqPtr, err := DecodeProtocolMessage(msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(readReqPtr, &req) {
|
||||
t.Errorf("got req=%#v, want %#v", readReqPtr, req)
|
||||
}
|
||||
}
|
||||
|
||||
const cancelReqString = "Content-Length: 75\r\n\r\n{\"seq\":25,\"type\":\"request\",\"command\":\"cancel\",\"arguments\":{\"requestId\":24}}"
|
||||
|
||||
var cancelReqStruct = CancelRequest{
|
||||
Request: Request{
|
||||
ProtocolMessage: ProtocolMessage{
|
||||
Type: "request",
|
||||
Seq: 25,
|
||||
},
|
||||
Command: "cancel",
|
||||
},
|
||||
Arguments: &CancelArguments{RequestId: 24},
|
||||
}
|
||||
|
||||
func TestWriteProtocolMessage(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := WriteProtocolMessage(buf, &cancelReqStruct)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if buf.String() != cancelReqString {
|
||||
t.Errorf("got %#v, want %#v", buf.String(), cancelReqString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadProtocolMessage(t *testing.T) {
|
||||
reader := bufio.NewReader(strings.NewReader(cancelReqString))
|
||||
|
||||
msg, err := ReadProtocolMessage(reader)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(msg, &cancelReqStruct) {
|
||||
t.Errorf("got req=%#v, want %#v", msg, &cancelReqStruct)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
// 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 dap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// makeErrorResponse creates a pre-populated ErrorResponse for testing.
|
||||
func makeErrorResponse() *ErrorResponse {
|
||||
return &ErrorResponse{
|
||||
Response: Response{
|
||||
ProtocolMessage: ProtocolMessage{
|
||||
Seq: 199,
|
||||
Type: "response",
|
||||
},
|
||||
Command: "stackTrace",
|
||||
RequestSeq: 9,
|
||||
Success: false,
|
||||
Message: "Unable to produce stack trace: \"{e}\"",
|
||||
},
|
||||
Body: ErrorResponseBody{
|
||||
Error: &ErrorMessage{
|
||||
Id: 2004,
|
||||
Format: "Unable to produce stack trace: \"{e}\"",
|
||||
Variables: map[string]string{"e": "Unknown goroutine 1"},
|
||||
ShowUser: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageInterface(t *testing.T) {
|
||||
resp := makeErrorResponse()
|
||||
f := func(m Message) int {
|
||||
return m.GetSeq()
|
||||
}
|
||||
// Test adherence to the Message interface.
|
||||
seq := f(resp)
|
||||
|
||||
if seq != 199 {
|
||||
t.Errorf("got seq=%d, want 199", seq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReponseMessageInterface(t *testing.T) {
|
||||
resp := makeErrorResponse()
|
||||
f := func(rm ResponseMessage) (int, int) {
|
||||
return rm.GetSeq(), rm.GetResponse().RequestSeq
|
||||
}
|
||||
// Test adherence to the ResponseMessage interface.
|
||||
seq, rseq := f(resp)
|
||||
|
||||
if seq != 199 {
|
||||
t.Errorf("got seq=%d, want 199", seq)
|
||||
}
|
||||
if rseq != 9 {
|
||||
t.Errorf("got ResponseSeq=%d, want 9", rseq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchAttachRequestInterface(t *testing.T) {
|
||||
lr := &LaunchRequest{
|
||||
Request: Request{
|
||||
ProtocolMessage: ProtocolMessage{
|
||||
Seq: 19,
|
||||
Type: "request",
|
||||
},
|
||||
Command: "launch",
|
||||
},
|
||||
Arguments: json.RawMessage("foobar"),
|
||||
}
|
||||
ar := &AttachRequest{
|
||||
Request: Request{
|
||||
ProtocolMessage: ProtocolMessage{
|
||||
Seq: 19,
|
||||
Type: "request",
|
||||
},
|
||||
Command: "attach",
|
||||
},
|
||||
Arguments: json.RawMessage(`{"foo":"bar"}`),
|
||||
}
|
||||
|
||||
f := func(r LaunchAttachRequest) (int, string, json.RawMessage) {
|
||||
return r.GetSeq(), r.GetRequest().Command, r.GetArguments()
|
||||
}
|
||||
// Test adherence to the LaunchAttachRequest interface.
|
||||
lseq, lcmd, lfoo := f(lr)
|
||||
aseq, acmd, afoo := f(ar)
|
||||
|
||||
if lseq != 19 || aseq != 19 {
|
||||
t.Errorf("got lseq=%d aseq=%d, want 19", lseq, aseq)
|
||||
}
|
||||
if lcmd != "launch" || acmd != "attach" {
|
||||
t.Errorf("got lcmd=%s acmd=%s, want (\"launch\", \"attach\")", lcmd, acmd)
|
||||
}
|
||||
if !bytes.Equal(lfoo, []byte("foobar")) || !bytes.Equal(afoo, []byte(`{"foo":"bar"}`)) {
|
||||
t.Errorf(`got lfoo=%v afoo=%v, want "foobar", {"foo":"bar"}`, lfoo, afoo)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user