service/dap: Initial implementation for 'dlv dap' (#1858)
* Initial implementation for 'dlv dap' * Fix Travis and AppVeyor failures * Address review comments * Address review comments * Regenrate documentation * Replace dap server printfs with log.Error * Update 'dap log' * Fix typos * Revert logflags changes that got mixed in by accident
This commit is contained in:
parent
8fa6d82177
commit
fbc4623c08
@ -38,6 +38,7 @@ Pass flags to the program you are debugging using `--`, for example:
|
|||||||
* [dlv attach](dlv_attach.md) - Attach to running process and begin debugging.
|
* [dlv attach](dlv_attach.md) - Attach to running process and begin debugging.
|
||||||
* [dlv connect](dlv_connect.md) - Connect to a headless debug server.
|
* [dlv connect](dlv_connect.md) - Connect to a headless debug server.
|
||||||
* [dlv core](dlv_core.md) - Examine a core dump.
|
* [dlv core](dlv_core.md) - Examine a core dump.
|
||||||
|
* [dlv dap](dlv_dap.md) - [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
|
||||||
* [dlv debug](dlv_debug.md) - Compile and begin debugging main package in current directory, or the package specified.
|
* [dlv debug](dlv_debug.md) - Compile and begin debugging main package in current directory, or the package specified.
|
||||||
* [dlv exec](dlv_exec.md) - Execute a precompiled binary, and begin a debug session.
|
* [dlv exec](dlv_exec.md) - Execute a precompiled binary, and begin a debug session.
|
||||||
* [dlv replay](dlv_replay.md) - Replays a rr trace.
|
* [dlv replay](dlv_replay.md) - Replays a rr trace.
|
||||||
|
|||||||
41
Documentation/usage/dlv_dap.md
Normal file
41
Documentation/usage/dlv_dap.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
## dlv dap
|
||||||
|
|
||||||
|
[EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
|
||||||
|
|
||||||
|
### Synopsis
|
||||||
|
|
||||||
|
|
||||||
|
[EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
|
||||||
|
|
||||||
|
The server supports debugging of a precompiled binary akin to 'dlv exec' via a launch request.
|
||||||
|
It does not yet support support specification of program arguments.
|
||||||
|
It does not yet support launch requests with 'debug' and 'test' modes that require compilation.
|
||||||
|
It does not yet support attach requests to debug a running process like with 'dlv attach'.
|
||||||
|
It does not yet support asynchronous request-response communication.
|
||||||
|
The server does not accept multiple client connections.
|
||||||
|
|
||||||
|
```
|
||||||
|
dlv dap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
--accept-multiclient Allows a headless server to accept multiple client connections.
|
||||||
|
--api-version int Selects API version when headless. (default 1)
|
||||||
|
--backend string Backend selection (see 'dlv help backend'). (default "default")
|
||||||
|
--build-flags string Build flags, to be passed to the compiler.
|
||||||
|
--check-go-version Checks that the version of Go in use is compatible with Delve. (default true)
|
||||||
|
--headless Run debug server only, in headless mode.
|
||||||
|
--init string Init file, executed by the terminal client.
|
||||||
|
-l, --listen string Debugging server listen address. (default "127.0.0.1:0")
|
||||||
|
--log Enable debugging server logging.
|
||||||
|
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
|
||||||
|
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
|
||||||
|
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
|
||||||
|
--wd string Working directory for running the program. (default ".")
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEE ALSO
|
||||||
|
* [dlv](dlv.md) - Delve is a debugger for the Go programming language.
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ names selected from this list:
|
|||||||
lldbout Copy output from debugserver/lldb to standard output
|
lldbout Copy output from debugserver/lldb to standard output
|
||||||
debuglineerr Log recoverable errors reading .debug_line
|
debuglineerr Log recoverable errors reading .debug_line
|
||||||
rpc Log all RPC messages
|
rpc Log all RPC messages
|
||||||
|
dap Log all DAP messages
|
||||||
fncall Log function call protocol
|
fncall Log function call protocol
|
||||||
minidump Log minidump loading
|
minidump Log minidump loading
|
||||||
|
|
||||||
@ -24,8 +25,8 @@ Additionally --log-dest can be used to specify where the logs should be
|
|||||||
written.
|
written.
|
||||||
If the argument is a number it will be interpreted as a file descriptor,
|
If the argument is a number it will be interpreted as a file descriptor,
|
||||||
otherwise as a file path.
|
otherwise as a file path.
|
||||||
This option will also redirect the \"API listening\" message in headless
|
This option will also redirect the "server listening at" message in headless
|
||||||
mode.
|
and dap modes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/go-delve/delve/pkg/version"
|
"github.com/go-delve/delve/pkg/version"
|
||||||
"github.com/go-delve/delve/service"
|
"github.com/go-delve/delve/service"
|
||||||
"github.com/go-delve/delve/service/api"
|
"github.com/go-delve/delve/service/api"
|
||||||
|
"github.com/go-delve/delve/service/dap"
|
||||||
"github.com/go-delve/delve/service/rpc2"
|
"github.com/go-delve/delve/service/rpc2"
|
||||||
"github.com/go-delve/delve/service/rpccommon"
|
"github.com/go-delve/delve/service/rpccommon"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -152,6 +153,22 @@ option to let the process continue or kill it.
|
|||||||
}
|
}
|
||||||
RootCommand.AddCommand(connectCommand)
|
RootCommand.AddCommand(connectCommand)
|
||||||
|
|
||||||
|
// 'dap' subcommand.
|
||||||
|
dapCommand := &cobra.Command{
|
||||||
|
Use: "dap",
|
||||||
|
Short: "[EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).",
|
||||||
|
Long: `[EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
|
||||||
|
|
||||||
|
The server supports debugging of a precompiled binary akin to 'dlv exec' via a launch request.
|
||||||
|
It does not yet support support specification of program arguments.
|
||||||
|
It does not yet support launch requests with 'debug' and 'test' modes that require compilation.
|
||||||
|
It does not yet support attach requests to debug a running process like with 'dlv attach'.
|
||||||
|
It does not yet support asynchronous request-response communication.
|
||||||
|
The server does not accept multiple client connections.`,
|
||||||
|
Run: dapCmd,
|
||||||
|
}
|
||||||
|
RootCommand.AddCommand(dapCommand)
|
||||||
|
|
||||||
// 'debug' subcommand.
|
// 'debug' subcommand.
|
||||||
debugCommand := &cobra.Command{
|
debugCommand := &cobra.Command{
|
||||||
Use: "debug [package]",
|
Use: "debug [package]",
|
||||||
@ -318,6 +335,7 @@ names selected from this list:
|
|||||||
lldbout Copy output from debugserver/lldb to standard output
|
lldbout Copy output from debugserver/lldb to standard output
|
||||||
debuglineerr Log recoverable errors reading .debug_line
|
debuglineerr Log recoverable errors reading .debug_line
|
||||||
rpc Log all RPC messages
|
rpc Log all RPC messages
|
||||||
|
dap Log all DAP messages
|
||||||
fncall Log function call protocol
|
fncall Log function call protocol
|
||||||
minidump Log minidump loading
|
minidump Log minidump loading
|
||||||
|
|
||||||
@ -325,8 +343,8 @@ Additionally --log-dest can be used to specify where the logs should be
|
|||||||
written.
|
written.
|
||||||
If the argument is a number it will be interpreted as a file descriptor,
|
If the argument is a number it will be interpreted as a file descriptor,
|
||||||
otherwise as a file path.
|
otherwise as a file path.
|
||||||
This option will also redirect the \"API listening\" message in headless
|
This option will also redirect the "server listening at" message in headless
|
||||||
mode.
|
and dap modes.
|
||||||
|
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ -344,6 +362,63 @@ func remove(path string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dapCmd(cmd *cobra.Command, args []string) {
|
||||||
|
status := func() int {
|
||||||
|
if err := logflags.Setup(Log, LogOutput, LogDest); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer logflags.Close()
|
||||||
|
|
||||||
|
if Headless {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: headless mode not supported with dap\n")
|
||||||
|
}
|
||||||
|
if AcceptMulti {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: accept multiclient mode not supported with dap\n")
|
||||||
|
}
|
||||||
|
if InitFile != "" {
|
||||||
|
fmt.Fprint(os.Stderr, "Warning: init file ignored with dap\n")
|
||||||
|
}
|
||||||
|
if ContinueOnStart {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: continue ignored with dap; specify via launch/attach request instead\n")
|
||||||
|
}
|
||||||
|
if BuildFlags != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: build flags ignored with dap; specify via launch/attach request instead\n")
|
||||||
|
}
|
||||||
|
if WorkingDir != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: working directory ignored with dap; launch requests must specify full program path\n")
|
||||||
|
}
|
||||||
|
dlvArgs, targetArgs := splitArgs(cmd, args)
|
||||||
|
if len(dlvArgs) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: debug arguments ignored with dap; specify via launch/attach request instead\n")
|
||||||
|
}
|
||||||
|
if len(targetArgs) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: program flags ignored with dap; specify via launch/attach request instead\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", Addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("couldn't start listener: %s\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
disconnectChan := make(chan struct{})
|
||||||
|
server := dap.NewServer(&service.Config{
|
||||||
|
Listener: listener,
|
||||||
|
Backend: Backend,
|
||||||
|
Foreground: true, // always headless
|
||||||
|
DebugInfoDirectories: conf.DebugInfoDirectories,
|
||||||
|
CheckGoVersion: CheckGoVersion,
|
||||||
|
DisconnectChan: disconnectChan,
|
||||||
|
})
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
server.Run()
|
||||||
|
waitForDisconnectSignal(disconnectChan)
|
||||||
|
return 0
|
||||||
|
}()
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
|
||||||
func debugCmd(cmd *cobra.Command, args []string) {
|
func debugCmd(cmd *cobra.Command, args []string) {
|
||||||
status := func() int {
|
status := func() int {
|
||||||
debugname, err := filepath.Abs(cmd.Flag("output").Value.String())
|
debugname, err := filepath.Abs(cmd.Flag("output").Value.String())
|
||||||
@ -535,6 +610,37 @@ func connectCmd(cmd *cobra.Command, args []string) {
|
|||||||
os.Exit(connect(addr, nil, conf, executingOther))
|
os.Exit(connect(addr, nil, conf, executingOther))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForDisconnectSignal is a blocking function that waits for either
|
||||||
|
// a SIGINT (Ctrl-C) signal from the OS or for disconnectChan to be closed
|
||||||
|
// by the server when the client disconnects.
|
||||||
|
// Note that in headless mode, the debugged process is foregrounded
|
||||||
|
// (to have control of the tty for debugging interactive programs),
|
||||||
|
// so SIGINT gets sent to the debuggee and not to delve.
|
||||||
|
func waitForDisconnectSignal(disconnectChan chan struct{}) {
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGINT)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// On windows Ctrl-C sent to inferior process is delivered
|
||||||
|
// as SIGINT to delve. Ignore it instead of stopping the server
|
||||||
|
// in order to be able to debug signal handlers.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-disconnectChan:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-disconnectChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) {
|
func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) {
|
||||||
if cmd.ArgsLenAtDash() >= 0 {
|
if cmd.ArgsLenAtDash() >= 0 {
|
||||||
return args[:cmd.ArgsLenAtDash()], args[cmd.ArgsLenAtDash():]
|
return args[:cmd.ArgsLenAtDash()], args[cmd.ArgsLenAtDash():]
|
||||||
@ -678,28 +784,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
|
|||||||
client = rpc2.NewClient(listener.Addr().String())
|
client = rpc2.NewClient(listener.Addr().String())
|
||||||
client.Disconnect(true) // true = continue after disconnect
|
client.Disconnect(true) // true = continue after disconnect
|
||||||
}
|
}
|
||||||
ch := make(chan os.Signal, 1)
|
waitForDisconnectSignal(disconnectChan)
|
||||||
signal.Notify(ch, syscall.SIGINT)
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// On windows Ctrl-C sent to inferior process is delivered
|
|
||||||
// as SIGINT to delve. Ignore it instead of stopping the server
|
|
||||||
// in order to be able to debug signal handlers.
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-disconnectChan:
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
case <-disconnectChan:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = server.Stop()
|
err = server.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -5,6 +5,7 @@ go 1.11
|
|||||||
require (
|
require (
|
||||||
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5
|
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5
|
||||||
github.com/cpuguy83/go-md2man v1.0.8 // indirect
|
github.com/cpuguy83/go-md2man v1.0.8 // indirect
|
||||||
|
github.com/google/go-dap v0.1.0
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561
|
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -8,6 +8,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-dap v0.1.0 h1:UzVzngq6yBR+bJVnTM8mjcdKrQnYR7m7U7JpNSnEo48=
|
||||||
|
github.com/google/go-dap v0.1.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
|||||||
@ -21,6 +21,7 @@ var gdbWire = false
|
|||||||
var lldbServerOutput = false
|
var lldbServerOutput = false
|
||||||
var debugLineErrors = false
|
var debugLineErrors = false
|
||||||
var rpc = false
|
var rpc = false
|
||||||
|
var dap = false
|
||||||
var fnCall = false
|
var fnCall = false
|
||||||
var minidump = false
|
var minidump = false
|
||||||
|
|
||||||
@ -82,6 +83,16 @@ func RPCLogger() *logrus.Entry {
|
|||||||
return makeLogger(rpc, logrus.Fields{"layer": "rpc"})
|
return makeLogger(rpc, logrus.Fields{"layer": "rpc"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DAP returns true if dap package should log.
|
||||||
|
func DAP() bool {
|
||||||
|
return dap
|
||||||
|
}
|
||||||
|
|
||||||
|
// DAPLogger returns a logger for dap package.
|
||||||
|
func DAPLogger() *logrus.Entry {
|
||||||
|
return makeLogger(dap, logrus.Fields{"layer": "dap"})
|
||||||
|
}
|
||||||
|
|
||||||
// FnCall returns true if the function call protocol should be logged.
|
// FnCall returns true if the function call protocol should be logged.
|
||||||
func FnCall() bool {
|
func FnCall() bool {
|
||||||
return fnCall
|
return fnCall
|
||||||
@ -100,12 +111,21 @@ func MinidumpLogger() *logrus.Entry {
|
|||||||
return makeLogger(minidump, logrus.Fields{"layer": "core", "kind": "minidump"})
|
return makeLogger(minidump, logrus.Fields{"layer": "core", "kind": "minidump"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteDAPListeningMessage writes the "DAP server listening" message in dap mode.
|
||||||
|
func WriteDAPListeningMessage(addr string) {
|
||||||
|
writeListeningMessage("DAP", addr)
|
||||||
|
}
|
||||||
|
|
||||||
// WriteAPIListeningMessage writes the "API server listening" message in headless mode.
|
// WriteAPIListeningMessage writes the "API server listening" message in headless mode.
|
||||||
func WriteAPIListeningMessage(addr string) {
|
func WriteAPIListeningMessage(addr string) {
|
||||||
|
writeListeningMessage("API", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeListeningMessage(server string, addr string) {
|
||||||
if logOut != nil {
|
if logOut != nil {
|
||||||
fmt.Fprintf(logOut, "API server listening at: %s\n", addr)
|
fmt.Fprintf(logOut, "%s server listening at: %s\n", server, addr)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("API server listening at: %s\n", addr)
|
fmt.Printf("%s server listening at: %s\n", server, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,10 +171,14 @@ func Setup(logFlag bool, logstr string, logDest string) error {
|
|||||||
debugLineErrors = true
|
debugLineErrors = true
|
||||||
case "rpc":
|
case "rpc":
|
||||||
rpc = true
|
rpc = true
|
||||||
|
case "dap":
|
||||||
|
dap = true
|
||||||
case "fncall":
|
case "fncall":
|
||||||
fnCall = true
|
fnCall = true
|
||||||
case "minidump":
|
case "minidump":
|
||||||
minidump = true
|
minidump = true
|
||||||
|
// If adding another value, do make sure to
|
||||||
|
// update "Help about logging flags" in commands.go.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
189
service/dap/daptest/client.go
Normal file
189
service/dap/daptest/client.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package daptest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/google/go-dap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package daptest provides a sample client with utilities
|
||||||
|
// for DAP mode testing.
|
||||||
|
|
||||||
|
// Client is a debugger service client that uses Debug Adaptor Protocol.
|
||||||
|
// It does not (yet?) implement service.Client interface.
|
||||||
|
// All client methods are synchronous.
|
||||||
|
type Client struct {
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
// seq is used to track the sequence number of each
|
||||||
|
// requests that the client sends to the server
|
||||||
|
seq int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Client over a TCP connection.
|
||||||
|
// Call Close() to close the connection.
|
||||||
|
func NewClient(addr string) *Client {
|
||||||
|
fmt.Println("Connecting to server at:", addr)
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("dialing:", err)
|
||||||
|
}
|
||||||
|
c := &Client{conn: conn, reader: bufio.NewReader(conn)}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the client connection
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) send(request dap.Message) {
|
||||||
|
jsonmsg, _ := json.Marshal(request)
|
||||||
|
fmt.Println("[client -> server]", string(jsonmsg))
|
||||||
|
dap.WriteProtocolMessage(c.conn, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBaseMessage reads and returns a json-encoded DAP message.
|
||||||
|
func (c *Client) ReadBaseMessage() ([]byte, error) {
|
||||||
|
message, err := dap.ReadBaseMessage(c.reader)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("DAP client error:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("[client <- server]", string(message))
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadErrorResponse reads, decodes and validates the result
|
||||||
|
// to be an error response. Returns the response or an error.
|
||||||
|
func (c *Client) ReadErrorResponse() (dap.Message, error) {
|
||||||
|
response, err := dap.ReadProtocolMessage(c.reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch response.(type) {
|
||||||
|
case *dap.ErrorResponse:
|
||||||
|
return response, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New(fmt.Sprintf("not an ErrorResponse: %#v", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeRequest sends an 'initialize' request.
|
||||||
|
func (c *Client) InitializeRequest() {
|
||||||
|
request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
|
||||||
|
request.Arguments = dap.InitializeRequestArguments{
|
||||||
|
AdapterID: "go",
|
||||||
|
PathFormat: "path",
|
||||||
|
LinesStartAt1: true,
|
||||||
|
ColumnsStartAt1: true,
|
||||||
|
SupportsVariableType: true,
|
||||||
|
SupportsVariablePaging: true,
|
||||||
|
SupportsRunInTerminalRequest: true,
|
||||||
|
Locale: "en-us",
|
||||||
|
}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchRequest sends a 'launch' request.
|
||||||
|
func (c *Client) LaunchRequest(program string, stopOnEntry bool) {
|
||||||
|
request := &dap.LaunchRequest{Request: *c.newRequest("launch")}
|
||||||
|
request.Arguments = map[string]interface{}{
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "exec",
|
||||||
|
"program": program,
|
||||||
|
"stopOnEntry": stopOnEntry,
|
||||||
|
}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectRequest sends a 'disconnect' request.
|
||||||
|
func (c *Client) DisconnectRequest() {
|
||||||
|
request := &dap.DisconnectRequest{Request: *c.newRequest("disconnect")}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBreakpointsRequest sends a 'setBreakpoints' request.
|
||||||
|
func (c *Client) SetBreakpointsRequest(file string, lines []int) {
|
||||||
|
request := &dap.SetBreakpointsRequest{Request: *c.newRequest("setBreakpoints")}
|
||||||
|
request.Arguments = dap.SetBreakpointsArguments{
|
||||||
|
Source: dap.Source{
|
||||||
|
Name: filepath.Base(file),
|
||||||
|
Path: file,
|
||||||
|
},
|
||||||
|
Breakpoints: make([]dap.SourceBreakpoint, len(lines)),
|
||||||
|
//sourceModified: false,
|
||||||
|
}
|
||||||
|
for i, l := range lines {
|
||||||
|
request.Arguments.Breakpoints[i].Line = l
|
||||||
|
}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExceptionBreakpointsRequest sends a 'setExceptionBreakpoints' request.
|
||||||
|
func (c *Client) SetExceptionBreakpointsRequest() {
|
||||||
|
request := &dap.SetBreakpointsRequest{Request: *c.newRequest("setExceptionBreakpoints")}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigurationDoneRequest sends a 'configurationDone' request.
|
||||||
|
func (c *Client) ConfigurationDoneRequest() {
|
||||||
|
request := &dap.ConfigurationDoneRequest{Request: *c.newRequest("configurationDone")}
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueRequest sends a 'continue' request.
|
||||||
|
func (c *Client) ContinueRequest(thread int) {
|
||||||
|
request := &dap.ContinueRequest{Request: *c.newRequest("continue")}
|
||||||
|
request.Arguments.ThreadId = thread
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownRequest triggers dap.DecodeProtocolMessageFieldError.
|
||||||
|
func (c *Client) UnkownRequest() {
|
||||||
|
request := c.newRequest("unknown")
|
||||||
|
c.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnkownProtocolMessage triggers dap.DecodeProtocolMessageFieldError.
|
||||||
|
func (c *Client) UnkownProtocolMessage() {
|
||||||
|
m := &dap.ProtocolMessage{}
|
||||||
|
m.Seq = -1
|
||||||
|
m.Type = "unknown"
|
||||||
|
c.send(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownEvent triggers dap.DecodeProtocolMessageFieldError.
|
||||||
|
func (c *Client) UnknownEvent() {
|
||||||
|
event := &dap.Event{}
|
||||||
|
event.Type = "event"
|
||||||
|
event.Seq = -1
|
||||||
|
event.Event = "unknown"
|
||||||
|
c.send(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownEvent passes decode checks, but delve has no 'case' to
|
||||||
|
// handle it. This behaves the same way a new request type
|
||||||
|
// added to go-dap, but not to delve.
|
||||||
|
func (c *Client) KnownEvent() {
|
||||||
|
event := &dap.Event{}
|
||||||
|
event.Type = "event"
|
||||||
|
event.Seq = -1
|
||||||
|
event.Event = "terminated"
|
||||||
|
c.send(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) newRequest(command string) *dap.Request {
|
||||||
|
request := &dap.Request{}
|
||||||
|
request.Type = "request"
|
||||||
|
request.Command = command
|
||||||
|
request.Seq = c.seq
|
||||||
|
c.seq++
|
||||||
|
return request
|
||||||
|
}
|
||||||
15
service/dap/error_ids.go
Normal file
15
service/dap/error_ids.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package dap
|
||||||
|
|
||||||
|
// Unique identifiers for messages returned for errors from requests.
|
||||||
|
const (
|
||||||
|
UnsupportedCommand int = 9999
|
||||||
|
|
||||||
|
// The values below come from the vscode-go debug adaptor.
|
||||||
|
// Although the spec says they should be unique, the adaptor
|
||||||
|
// reuses 3000 for launch, attach and program exit failures.
|
||||||
|
// TODO(polina): confirm if the extension expects specific ids
|
||||||
|
// for specific cases, and we must match the existing adaptor
|
||||||
|
// or if these codes can evolve.
|
||||||
|
FailedToContinue = 3000
|
||||||
|
// Add more codes as we support more requests
|
||||||
|
)
|
||||||
466
service/dap/server.go
Normal file
466
service/dap/server.go
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
package dap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-delve/delve/pkg/logflags"
|
||||||
|
"github.com/go-delve/delve/pkg/proc"
|
||||||
|
"github.com/go-delve/delve/service"
|
||||||
|
"github.com/go-delve/delve/service/api"
|
||||||
|
"github.com/go-delve/delve/service/debugger"
|
||||||
|
"github.com/google/go-dap"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package dap implements VSCode's Debug Adaptor Protocol (DAP).
|
||||||
|
// This allows delve to communicate with frontends using DAP
|
||||||
|
// without a separate adaptor. The frontend will run the debugger
|
||||||
|
// (which now doubles as an adaptor) in server mode listening on
|
||||||
|
// a port and communicating over TCP. This is work in progress,
|
||||||
|
// so for now Delve in dap mode only supports synchronous
|
||||||
|
// request-response communication, blocking while processing each request.
|
||||||
|
// For DAP details see https://microsoft.github.io/debug-adapter-protocol.
|
||||||
|
|
||||||
|
// Server implements a DAP server that can accept a single client for
|
||||||
|
// a single debug session. It does not support restarting.
|
||||||
|
// The server operates via two goroutines:
|
||||||
|
// (1) Main goroutine where the server is created via NewServer(),
|
||||||
|
// started via Run() and stopped via Stop().
|
||||||
|
// (2) Run goroutine started from Run() that accepts a client connection,
|
||||||
|
// reads, decodes and processes each request, issuing commands to the
|
||||||
|
// underlying debugger and sending back events and responses.
|
||||||
|
// TODO(polina): make it asynchronous (i.e. launch goroutine per request)
|
||||||
|
type Server struct {
|
||||||
|
// config is all the information necessary to start the debugger and server.
|
||||||
|
config *service.Config
|
||||||
|
// listener is used to accept the client connection.
|
||||||
|
listener net.Listener
|
||||||
|
// conn is the accepted client connection.
|
||||||
|
conn net.Conn
|
||||||
|
// reader is used to read requests from the connection.
|
||||||
|
reader *bufio.Reader
|
||||||
|
// debugger is the underlying debugger service.
|
||||||
|
debugger *debugger.Debugger
|
||||||
|
// log is used for structured logging.
|
||||||
|
log *logrus.Entry
|
||||||
|
// stopOnEntry is set to automatically stop the debugee after start.
|
||||||
|
stopOnEntry bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates a new DAP Server. It takes an opened Listener
|
||||||
|
// via config and assumes its ownership. Optionally takes DisconnectChan
|
||||||
|
// via config, which can be used to detect when the client disconnects
|
||||||
|
// and the server is ready to be shut down. The caller must call
|
||||||
|
// Stop() on shutdown.
|
||||||
|
func NewServer(config *service.Config) *Server {
|
||||||
|
logger := logflags.DAPLogger()
|
||||||
|
logflags.WriteDAPListeningMessage(config.Listener.Addr().String())
|
||||||
|
return &Server{
|
||||||
|
config: config,
|
||||||
|
listener: config.Listener,
|
||||||
|
log: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the DAP debugger service, closes the listener and
|
||||||
|
// the client connection. It shuts down the underlying debugger
|
||||||
|
// and kills the target process if it was launched by it.
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
s.listener.Close()
|
||||||
|
if s.conn != nil {
|
||||||
|
// Unless Stop() was called after serveDAPCodec()
|
||||||
|
// returned, this will result in closed connection error
|
||||||
|
// on next read, breaking out of the read loop and
|
||||||
|
// allowing the run goroutine to exit.
|
||||||
|
s.conn.Close()
|
||||||
|
}
|
||||||
|
if s.debugger != nil {
|
||||||
|
kill := s.config.AttachPid == 0
|
||||||
|
if err := s.debugger.Detach(kill); err != nil {
|
||||||
|
s.log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// signalDisconnect closes config.DisconnectChan if not nil, which
|
||||||
|
// signals that the client disconnected or there was a client
|
||||||
|
// connection failure. Since the server currently services only one
|
||||||
|
// client, this can be used as a signal to the entire server via
|
||||||
|
// Stop(). The function safeguards agaist closing the channel more
|
||||||
|
// than once and can be called multiple times. It is not thread-safe
|
||||||
|
// and is currently only called from the run goroutine.
|
||||||
|
// TODO(polina): lock this when we add more goroutines that could call
|
||||||
|
// this when we support asynchronous request-response communication.
|
||||||
|
func (s *Server) signalDisconnect() {
|
||||||
|
// DisconnectChan might be nil at server creation if the
|
||||||
|
// caller does not want to rely on the disconnect signal.
|
||||||
|
if s.config.DisconnectChan != nil {
|
||||||
|
close(s.config.DisconnectChan)
|
||||||
|
// Take advantage of the nil check above to avoid accidentally
|
||||||
|
// closing the channel twice and causing a panic, when this
|
||||||
|
// function is called more than once. For example, we could
|
||||||
|
// have the following sequence of events:
|
||||||
|
// -- run goroutine: calls onDisconnectRequest()
|
||||||
|
// -- run goroutine: calls signalDisconnect()
|
||||||
|
// -- main goroutine: calls Stop()
|
||||||
|
// -- main goroutine: Stop() closes client connection
|
||||||
|
// -- run goroutine: serveDAPCodec() gets "closed network connection"
|
||||||
|
// -- run goroutine: serveDAPCodec() returns
|
||||||
|
// -- run goroutine: serveDAPCodec calls signalDisconnect()
|
||||||
|
s.config.DisconnectChan = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run launches a new goroutine where it accepts a client connection
|
||||||
|
// and starts processing requests from it. Use Stop() to close connection.
|
||||||
|
// The server does not support multiple clients, serially or in parallel.
|
||||||
|
// The server should be restarted for every new debug session.
|
||||||
|
// The debugger won't be started until launch/attach request is received.
|
||||||
|
// TODO(polina): allow new client connections for new debug sessions,
|
||||||
|
// so the editor needs to launch delve only once?
|
||||||
|
func (s *Server) Run() {
|
||||||
|
go func() {
|
||||||
|
conn, err := s.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
// This will print if the server is killed with Ctrl+C
|
||||||
|
// before client connection is accepted.
|
||||||
|
s.log.Errorf("Error accepting client connection: %s\n", err)
|
||||||
|
s.signalDisconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.conn = conn
|
||||||
|
s.serveDAPCodec()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveDAPCodec reads and decodes requests from the client
|
||||||
|
// until it encounters an error or EOF, when it sends
|
||||||
|
// the disconnect signal and returns.
|
||||||
|
func (s *Server) serveDAPCodec() {
|
||||||
|
defer s.signalDisconnect()
|
||||||
|
s.reader = bufio.NewReader(s.conn)
|
||||||
|
for {
|
||||||
|
request, err := dap.ReadProtocolMessage(s.reader)
|
||||||
|
// TODO(polina): Differentiate between errors and handle them
|
||||||
|
// gracefully. For example,
|
||||||
|
// -- "use of closed network connection" means client connection
|
||||||
|
// was closed via Stop() in response to a disconnect request.
|
||||||
|
// -- "Request command 'foo' is not supported" means we
|
||||||
|
// potentially got some new DAP request that we do not yet have
|
||||||
|
// decoding support for, so we can respond with an ErrorResponse.
|
||||||
|
// TODO(polina): to support this add Seq to
|
||||||
|
// dap.DecodeProtocolMessageFieldError.
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
s.log.Error("DAP error: ", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(polina) Add a panic guard,
|
||||||
|
// so we do not kill user's process when delve panics.
|
||||||
|
s.handleRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleRequest(request dap.Message) {
|
||||||
|
jsonmsg, _ := json.Marshal(request)
|
||||||
|
s.log.Debug("[<- from client]", string(jsonmsg))
|
||||||
|
|
||||||
|
switch request := request.(type) {
|
||||||
|
case *dap.InitializeRequest:
|
||||||
|
s.onInitializeRequest(request)
|
||||||
|
case *dap.LaunchRequest:
|
||||||
|
s.onLaunchRequest(request)
|
||||||
|
case *dap.AttachRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.DisconnectRequest:
|
||||||
|
s.onDisconnectRequest(request)
|
||||||
|
case *dap.TerminateRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.RestartRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SetBreakpointsRequest:
|
||||||
|
s.onSetBreakpointsRequest(request)
|
||||||
|
case *dap.SetFunctionBreakpointsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SetExceptionBreakpointsRequest:
|
||||||
|
s.onSetExceptionBreakpointsRequest(request)
|
||||||
|
case *dap.ConfigurationDoneRequest:
|
||||||
|
s.onConfigurationDoneRequest(request)
|
||||||
|
case *dap.ContinueRequest:
|
||||||
|
s.onContinueRequest(request)
|
||||||
|
case *dap.NextRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.StepInRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.StepOutRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.StepBackRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.ReverseContinueRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.RestartFrameRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.GotoRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.PauseRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.StackTraceRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.ScopesRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.VariablesRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SetVariableRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SetExpressionRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SourceRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.ThreadsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.TerminateThreadsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.EvaluateRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.StepInTargetsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.GotoTargetsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.CompletionsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.ExceptionInfoRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.LoadedSourcesRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.DataBreakpointInfoRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.SetDataBreakpointsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.ReadMemoryRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.DisassembleRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.CancelRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
case *dap.BreakpointLocationsRequest:
|
||||||
|
s.sendUnsupportedErrorResponse(request.Request)
|
||||||
|
default:
|
||||||
|
// This is a DAP message that go-dap has a struct for, so
|
||||||
|
// decoding succeeded, but this function does not know how
|
||||||
|
// to handle. We should be sending an ErrorResponse, but
|
||||||
|
// we cannot get to Seq and other fields from dap.Message.
|
||||||
|
// TODO(polina): figure out how to handle this better.
|
||||||
|
// Consider adding GetSeq() method to dap.Message interface.
|
||||||
|
s.log.Errorf("Unable to process %#v\n", request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) send(message dap.Message) {
|
||||||
|
jsonmsg, _ := json.Marshal(message)
|
||||||
|
s.log.Debug("[-> to client]", string(jsonmsg))
|
||||||
|
dap.WriteProtocolMessage(s.conn, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
|
||||||
|
// TODO(polina): Respond with an error if debug session is in progress?
|
||||||
|
response := &dap.InitializeResponse{Response: *newResponse(request.Request)}
|
||||||
|
response.Body.SupportsConfigurationDoneRequest = true
|
||||||
|
// TODO(polina): support this to match vscode-go functionality
|
||||||
|
response.Body.SupportsSetVariable = false
|
||||||
|
s.send(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
||||||
|
// TODO(polina): Respond with an error if debug session is in progress?
|
||||||
|
program, ok := request.Arguments["program"]
|
||||||
|
if !ok || program == "" {
|
||||||
|
s.sendErrorResponse(request.Request,
|
||||||
|
FailedToContinue, "Failed to launch",
|
||||||
|
"The program attribute is missing in debug configuration.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.config.ProcessArgs = []string{program.(string)}
|
||||||
|
s.config.WorkingDir = filepath.Dir(program.(string))
|
||||||
|
// TODO: support program args
|
||||||
|
|
||||||
|
stop, ok := request.Arguments["stopOnEntry"]
|
||||||
|
s.stopOnEntry = (ok && stop == true)
|
||||||
|
|
||||||
|
mode, ok := request.Arguments["mode"]
|
||||||
|
if !ok || mode == "" {
|
||||||
|
mode = "debug"
|
||||||
|
}
|
||||||
|
// TODO(polina): support "debug", "test" and "remote" modes
|
||||||
|
if mode != "exec" {
|
||||||
|
s.sendErrorResponse(request.Request,
|
||||||
|
FailedToContinue, "Failed to launch",
|
||||||
|
fmt.Sprintf("Unsupported 'mode' value '%s' in debug configuration.", mode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &debugger.Config{
|
||||||
|
WorkingDir: s.config.WorkingDir,
|
||||||
|
AttachPid: 0,
|
||||||
|
CoreFile: "",
|
||||||
|
Backend: s.config.Backend,
|
||||||
|
Foreground: s.config.Foreground,
|
||||||
|
DebugInfoDirectories: s.config.DebugInfoDirectories,
|
||||||
|
CheckGoVersion: s.config.CheckGoVersion,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if s.debugger, err = debugger.New(config, s.config.ProcessArgs); err != nil {
|
||||||
|
s.sendErrorResponse(request.Request,
|
||||||
|
FailedToContinue, "Failed to launch", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the client that the debugger is ready to start accepting
|
||||||
|
// configuration requests for setting breakpoints, etc. The client
|
||||||
|
// will end the configuration sequence with 'configurationDone'.
|
||||||
|
s.send(&dap.InitializedEvent{Event: *newEvent("initialized")})
|
||||||
|
s.send(&dap.LaunchResponse{Response: *newResponse(request.Request)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// onDisconnectRequest handles the DisconnectRequest. Per the DAP spec,
|
||||||
|
// it disconnects the debuggee and signals that the debug adaptor
|
||||||
|
// (in our case this TCP server) can be terminated.
|
||||||
|
func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
|
||||||
|
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
|
||||||
|
if s.debugger != nil {
|
||||||
|
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(err)
|
||||||
|
}
|
||||||
|
kill := s.config.AttachPid == 0
|
||||||
|
err = s.debugger.Detach(kill)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(polina): make thread-safe when handlers become asynchronous.
|
||||||
|
s.signalDisconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
|
||||||
|
if request.Arguments.Source.Path == "" {
|
||||||
|
s.log.Error("ERROR: Unable to set breakpoint for empty file path")
|
||||||
|
}
|
||||||
|
response := &dap.SetBreakpointsResponse{Response: *newResponse(request.Request)}
|
||||||
|
response.Body.Breakpoints = make([]dap.Breakpoint, len(request.Arguments.Breakpoints))
|
||||||
|
// Only verified breakpoints will be set and reported back in the
|
||||||
|
// response. All breakpoints resulting in errors (e.g. duplicates
|
||||||
|
// or lines that do not have statements) will be skipped.
|
||||||
|
i := 0
|
||||||
|
for _, b := range request.Arguments.Breakpoints {
|
||||||
|
bp, err := s.debugger.CreateBreakpoint(
|
||||||
|
&api.Breakpoint{File: request.Arguments.Source.Path, Line: b.Line})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("ERROR:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
response.Body.Breakpoints[i].Verified = true
|
||||||
|
response.Body.Breakpoints[i].Line = bp.Line
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
response.Body.Breakpoints = response.Body.Breakpoints[:i]
|
||||||
|
s.send(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
|
||||||
|
// Unlike what DAP documentation claims, this request is always sent
|
||||||
|
// even though we specified no filters at initializatin. Handle as no-op.
|
||||||
|
s.send(&dap.SetExceptionBreakpointsResponse{Response: *newResponse(request.Request)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest) {
|
||||||
|
if s.stopOnEntry {
|
||||||
|
e := &dap.StoppedEvent{
|
||||||
|
Event: *newEvent("stopped"),
|
||||||
|
Body: dap.StoppedEventBody{Reason: "breakpoint", ThreadId: 1, AllThreadsStopped: true},
|
||||||
|
}
|
||||||
|
s.send(e)
|
||||||
|
}
|
||||||
|
s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)})
|
||||||
|
if !s.stopOnEntry {
|
||||||
|
s.doContinue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) onContinueRequest(request *dap.ContinueRequest) {
|
||||||
|
s.send(&dap.ContinueResponse{Response: *newResponse(request.Request)})
|
||||||
|
s.doContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendErrorResponse(request dap.Request, id int, summary string, details string) {
|
||||||
|
er := &dap.ErrorResponse{}
|
||||||
|
er.Type = "response"
|
||||||
|
er.Command = request.Command
|
||||||
|
er.RequestSeq = request.Seq
|
||||||
|
er.Success = false
|
||||||
|
er.Message = summary
|
||||||
|
er.Body.Error.Id = id
|
||||||
|
er.Body.Error.Format = fmt.Sprintf("%s: %s", summary, details)
|
||||||
|
s.log.Error(er.Body.Error.Format)
|
||||||
|
s.send(er)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendUnsupportedErrorResponse(request dap.Request) {
|
||||||
|
s.sendErrorResponse(request, UnsupportedCommand, "Unsupported command",
|
||||||
|
fmt.Sprintf("cannot process '%s' request", request.Command))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResponse(request dap.Request) *dap.Response {
|
||||||
|
return &dap.Response{
|
||||||
|
ProtocolMessage: dap.ProtocolMessage{
|
||||||
|
Seq: 0,
|
||||||
|
Type: "response",
|
||||||
|
},
|
||||||
|
Command: request.Command,
|
||||||
|
RequestSeq: request.Seq,
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEvent(event string) *dap.Event {
|
||||||
|
return &dap.Event{
|
||||||
|
ProtocolMessage: dap.ProtocolMessage{
|
||||||
|
Seq: 0,
|
||||||
|
Type: "event",
|
||||||
|
},
|
||||||
|
Event: event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) doContinue() {
|
||||||
|
if s.debugger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Continue})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(err)
|
||||||
|
switch err.(type) {
|
||||||
|
case proc.ErrProcessExited:
|
||||||
|
e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
|
||||||
|
s.send(e)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.Exited {
|
||||||
|
e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
|
||||||
|
s.send(e)
|
||||||
|
} else {
|
||||||
|
e := &dap.StoppedEvent{Event: *newEvent("stopped")}
|
||||||
|
// TODO(polina): differentiate between breakpoint and pause on halt.
|
||||||
|
e.Body.Reason = "breakpoint"
|
||||||
|
e.Body.AllThreadsStopped = true
|
||||||
|
e.Body.ThreadId = state.SelectedGoroutine.ID
|
||||||
|
s.send(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
160
service/dap/server_test.go
Normal file
160
service/dap/server_test.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package dap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-delve/delve/pkg/logflags"
|
||||||
|
protest "github.com/go-delve/delve/pkg/proc/test"
|
||||||
|
"github.com/go-delve/delve/service"
|
||||||
|
"github.com/go-delve/delve/service/dap/daptest"
|
||||||
|
"github.com/google/go-dap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var logOutput string
|
||||||
|
flag.StringVar(&logOutput, "log-output", "", "configures log output")
|
||||||
|
flag.Parse()
|
||||||
|
logflags.Setup(logOutput != "", logOutput, "")
|
||||||
|
os.Exit(protest.RunTestsWithFixtures(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDAPServer(t *testing.T) (server *Server, addr string) {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server = NewServer(&service.Config{
|
||||||
|
Listener: listener,
|
||||||
|
Backend: "default",
|
||||||
|
DisconnectChan: nil,
|
||||||
|
})
|
||||||
|
server.Run()
|
||||||
|
// Give server time to start listening for clients
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
return server, listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectMessage(t *testing.T, client *daptest.Client, want []byte) {
|
||||||
|
got, err := client.ReadBaseMessage()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("\ngot %q\nwant %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// name is for _fixtures/<name>.go
|
||||||
|
func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.Fixture)) {
|
||||||
|
var buildFlags protest.BuildFlags
|
||||||
|
fixture := protest.BuildFixture(name, buildFlags)
|
||||||
|
|
||||||
|
server, addr := startDAPServer(t)
|
||||||
|
client := daptest.NewClient(addr)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
test(client, fixture)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(polina): instead of hardcoding message bytes,
|
||||||
|
// add methods to client to receive, decode and verify responses.
|
||||||
|
|
||||||
|
func TestStopOnEntry(t *testing.T) {
|
||||||
|
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
|
client.InitializeRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":0,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true}}`))
|
||||||
|
|
||||||
|
client.LaunchRequest(fixture.Path, true /*stopOnEntry*/)
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"initialized"}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":1,"success":true,"command":"launch"}`))
|
||||||
|
|
||||||
|
client.SetExceptionBreakpointsRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":2,"success":true,"command":"setExceptionBreakpoints"}`))
|
||||||
|
|
||||||
|
client.ConfigurationDoneRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"stopped","body":{"reason":"breakpoint","threadId":1,"allThreadsStopped":true}}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":3,"success":true,"command":"configurationDone"}`))
|
||||||
|
|
||||||
|
client.ContinueRequest(1)
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":4,"success":true,"command":"continue","body":{}}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"terminated","body":{}}`))
|
||||||
|
|
||||||
|
client.DisconnectRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":5,"success":true,"command":"disconnect"}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetBreakpoint(t *testing.T) {
|
||||||
|
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
|
client.InitializeRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":0,"success":true,"command":"initialize","body":{"supportsConfigurationDoneRequest":true}}`))
|
||||||
|
|
||||||
|
client.LaunchRequest(fixture.Path, false /*stopOnEntry*/)
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"initialized"}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":1,"success":true,"command":"launch"}`))
|
||||||
|
|
||||||
|
client.SetBreakpointsRequest(fixture.Source, []int{8, 100})
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":2,"success":true,"command":"setBreakpoints","body":{"breakpoints":[{"verified":true,"source":{},"line":8}]}}`))
|
||||||
|
|
||||||
|
client.SetExceptionBreakpointsRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":3,"success":true,"command":"setExceptionBreakpoints"}`))
|
||||||
|
|
||||||
|
client.ConfigurationDoneRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":4,"success":true,"command":"configurationDone"}`))
|
||||||
|
|
||||||
|
client.ContinueRequest(1)
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"stopped","body":{"reason":"breakpoint","threadId":1,"allThreadsStopped":true}}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":5,"success":true,"command":"continue","body":{}}`))
|
||||||
|
|
||||||
|
client.ContinueRequest(1)
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"event","event":"terminated","body":{}}`))
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":6,"success":true,"command":"continue","body":{}}`))
|
||||||
|
|
||||||
|
client.DisconnectRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":7,"success":true,"command":"disconnect"}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectErrorResponse(t *testing.T, client *daptest.Client, requestSeq int, command string, message string, id int) *dap.ErrorResponse {
|
||||||
|
response, err := client.ReadErrorResponse()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
got := response.(*dap.ErrorResponse)
|
||||||
|
if got.RequestSeq != requestSeq || got.Command != command || got.Message != message || got.Body.Error.Id != id {
|
||||||
|
want := fmt.Sprintf("{RequestSeq: %d, Command: %q, Message: %q, Id: %d}", requestSeq, command, message, id)
|
||||||
|
t.Errorf("\ngot %#v\nwant %s", got, want)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadLaunchRequests(t *testing.T) {
|
||||||
|
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
|
client.LaunchRequest("", true)
|
||||||
|
// Test for the DAP-specific detailed error message.
|
||||||
|
want := "Failed to launch: The program attribute is missing in debug configuration."
|
||||||
|
if got := expectErrorResponse(t, client, 0, "launch", "Failed to launch", 3000); got != nil && got.Body.Error.Format != want {
|
||||||
|
t.Errorf("got %q, want %q", got.Body.Error.Format, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip detailed message checks for potentially different OS-specific errors.
|
||||||
|
client.LaunchRequest(fixture.Path+"_does_not_exist", false)
|
||||||
|
expectErrorResponse(t, client, 1, "launch", "Failed to launch", 3000)
|
||||||
|
|
||||||
|
client.LaunchRequest(fixture.Source, true) // Not an executable
|
||||||
|
expectErrorResponse(t, client, 2, "launch", "Failed to launch", 3000)
|
||||||
|
|
||||||
|
// We failed to launch the program. Make sure shutdown still works.
|
||||||
|
client.DisconnectRequest()
|
||||||
|
expectMessage(t, client, []byte(`{"seq":0,"type":"response","request_seq":3,"success":true,"command":"disconnect"}`))
|
||||||
|
})
|
||||||
|
}
|
||||||
12
vendor/github.com/google/go-dap/.travis.yml
generated
vendored
Normal file
12
vendor/github.com/google/go-dap/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.13.x
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- GOPROXY=https://proxy.golang.org
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
|
script:
|
||||||
|
- 'internal/test.sh'
|
||||||
202
vendor/github.com/google/go-dap/LICENSE
generated
vendored
Normal file
202
vendor/github.com/google/go-dap/LICENSE
generated
vendored
Normal file
@ -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.
|
||||||
19
vendor/github.com/google/go-dap/README.md
generated
vendored
Normal file
19
vendor/github.com/google/go-dap/README.md
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# go-dap: Go implementation of the Debug Adapter Protocol
|
||||||
|
|
||||||
|
[](https://travis-ci.org/google/go-dap)
|
||||||
|
[](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.
|
||||||
219
vendor/github.com/google/go-dap/codec.go
generated
vendored
Normal file
219
vendor/github.com/google/go-dap/codec.go
generated
vendored
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2019 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 {
|
||||||
|
SubType string
|
||||||
|
FieldName string
|
||||||
|
FieldValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DecodeProtocolMessageFieldError) Error() string {
|
||||||
|
return fmt.Sprintf("%s %s '%s' is not supported", e.SubType, e.FieldName, e.FieldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeProtocolMessage 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 DecodeProtocolMessage(data []byte) (Message, error) {
|
||||||
|
var protomsg ProtocolMessage
|
||||||
|
if err := json.Unmarshal(data, &protomsg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch protomsg.Type {
|
||||||
|
case "request":
|
||||||
|
return decodeRequest(data)
|
||||||
|
case "response":
|
||||||
|
return decodeResponse(data)
|
||||||
|
case "event":
|
||||||
|
return decodeEvent(data)
|
||||||
|
default:
|
||||||
|
return nil, &DecodeProtocolMessageFieldError{"ProtocolMessage", "type", protomsg.Type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageCtor func() Message
|
||||||
|
|
||||||
|
// 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 decodeRequest(data []byte) (Message, error) {
|
||||||
|
var r Request
|
||||||
|
if err := json.Unmarshal(data, &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ctor, ok := requestCtor[r.Command]; ok {
|
||||||
|
requestPtr := ctor()
|
||||||
|
err := json.Unmarshal(data, requestPtr)
|
||||||
|
return requestPtr, err
|
||||||
|
}
|
||||||
|
return nil, &DecodeProtocolMessageFieldError{"Request", "command", r.Command}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping of request commands and corresponding struct constructors that
|
||||||
|
// can be passed to json.Unmarshal.
|
||||||
|
var requestCtor = map[string]messageCtor{
|
||||||
|
"cancel": func() Message { return &CancelRequest{} },
|
||||||
|
"runInTerminal": func() Message { return &RunInTerminalRequest{} },
|
||||||
|
"initialize": func() Message { return &InitializeRequest{} },
|
||||||
|
"configurationDone": func() Message { return &ConfigurationDoneRequest{} },
|
||||||
|
"launch": func() Message { return &LaunchRequest{} },
|
||||||
|
"attach": func() Message { return &AttachRequest{} },
|
||||||
|
"restart": func() Message { return &RestartRequest{} },
|
||||||
|
"disconnect": func() Message { return &DisconnectRequest{} },
|
||||||
|
"terminate": func() Message { return &TerminateRequest{} },
|
||||||
|
"breakpointLocations": func() Message { return &BreakpointLocationsRequest{} },
|
||||||
|
"setBreakpoints": func() Message { return &SetBreakpointsRequest{} },
|
||||||
|
"setFunctionBreakpoints": func() Message { return &SetFunctionBreakpointsRequest{} },
|
||||||
|
"setExceptionBreakpoints": func() Message { return &SetExceptionBreakpointsRequest{} },
|
||||||
|
"dataBreakpointInfo": func() Message { return &DataBreakpointInfoRequest{} },
|
||||||
|
"setDataBreakpoints": func() Message { return &SetDataBreakpointsRequest{} },
|
||||||
|
"continue": func() Message { return &ContinueRequest{} },
|
||||||
|
"next": func() Message { return &NextRequest{} },
|
||||||
|
"stepIn": func() Message { return &StepInRequest{} },
|
||||||
|
"stepOut": func() Message { return &StepOutRequest{} },
|
||||||
|
"stepBack": func() Message { return &StepBackRequest{} },
|
||||||
|
"reverseContinue": func() Message { return &ReverseContinueRequest{} },
|
||||||
|
"restartFrame": func() Message { return &RestartFrameRequest{} },
|
||||||
|
"goto": func() Message { return &GotoRequest{} },
|
||||||
|
"pause": func() Message { return &PauseRequest{} },
|
||||||
|
"stackTrace": func() Message { return &StackTraceRequest{} },
|
||||||
|
"scopes": func() Message { return &ScopesRequest{} },
|
||||||
|
"variables": func() Message { return &VariablesRequest{} },
|
||||||
|
"setVariable": func() Message { return &SetVariableRequest{} },
|
||||||
|
"source": func() Message { return &SourceRequest{} },
|
||||||
|
"threads": func() Message { return &ThreadsRequest{} },
|
||||||
|
"terminateThreads": func() Message { return &TerminateThreadsRequest{} },
|
||||||
|
"modules": func() Message { return &ModulesRequest{} },
|
||||||
|
"loadedSources": func() Message { return &LoadedSourcesRequest{} },
|
||||||
|
"evaluate": func() Message { return &EvaluateRequest{} },
|
||||||
|
"setExpression": func() Message { return &SetExpressionRequest{} },
|
||||||
|
"stepInTargets": func() Message { return &StepInTargetsRequest{} },
|
||||||
|
"gotoTargets": func() Message { return &GotoTargetsRequest{} },
|
||||||
|
"completions": func() Message { return &CompletionsRequest{} },
|
||||||
|
"exceptionInfo": func() Message { return &ExceptionInfoRequest{} },
|
||||||
|
"readMemory": func() Message { return &ReadMemoryRequest{} },
|
||||||
|
"disassemble": func() Message { return &DisassembleRequest{} },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 decodeResponse(data []byte) (Message, error) {
|
||||||
|
var r Response
|
||||||
|
if err := json.Unmarshal(data, &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !r.Success {
|
||||||
|
var er ErrorResponse
|
||||||
|
err := json.Unmarshal(data, &er)
|
||||||
|
return &er, err
|
||||||
|
}
|
||||||
|
if ctor, ok := responseCtor[r.Command]; ok {
|
||||||
|
responsePtr := ctor()
|
||||||
|
err := json.Unmarshal(data, responsePtr)
|
||||||
|
return responsePtr, err
|
||||||
|
}
|
||||||
|
return nil, &DecodeProtocolMessageFieldError{"Response", "command", r.Command}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping of response commands and corresponding struct constructors that
|
||||||
|
// can be passed to json.Unmarshal.
|
||||||
|
var responseCtor = map[string]messageCtor{
|
||||||
|
"cancel": func() Message { return &CancelResponse{} },
|
||||||
|
"runInTerminal": func() Message { return &RunInTerminalResponse{} },
|
||||||
|
"initialize": func() Message { return &InitializeResponse{} },
|
||||||
|
"configurationDone": func() Message { return &ConfigurationDoneResponse{} },
|
||||||
|
"launch": func() Message { return &LaunchResponse{} },
|
||||||
|
"attach": func() Message { return &AttachResponse{} },
|
||||||
|
"restart": func() Message { return &RestartResponse{} },
|
||||||
|
"disconnect": func() Message { return &DisconnectResponse{} },
|
||||||
|
"terminate": func() Message { return &TerminateResponse{} },
|
||||||
|
"breakpointLocations": func() Message { return &BreakpointLocationsResponse{} },
|
||||||
|
"setBreakpoints": func() Message { return &SetBreakpointsResponse{} },
|
||||||
|
"setFunctionBreakpoints": func() Message { return &SetFunctionBreakpointsResponse{} },
|
||||||
|
"setExceptionBreakpoints": func() Message { return &SetExceptionBreakpointsResponse{} },
|
||||||
|
"dataBreakpointInfo": func() Message { return &DataBreakpointInfoResponse{} },
|
||||||
|
"setDataBreakpoints": func() Message { return &SetDataBreakpointsResponse{} },
|
||||||
|
"continue": func() Message { return &ContinueResponse{} },
|
||||||
|
"next": func() Message { return &NextResponse{} },
|
||||||
|
"stepIn": func() Message { return &StepInResponse{} },
|
||||||
|
"stepOut": func() Message { return &StepOutResponse{} },
|
||||||
|
"stepBack": func() Message { return &StepBackResponse{} },
|
||||||
|
"reverseContinue": func() Message { return &ReverseContinueResponse{} },
|
||||||
|
"restartFrame": func() Message { return &RestartFrameResponse{} },
|
||||||
|
"goto": func() Message { return &GotoResponse{} },
|
||||||
|
"pause": func() Message { return &PauseResponse{} },
|
||||||
|
"stackTrace": func() Message { return &StackTraceResponse{} },
|
||||||
|
"scopes": func() Message { return &ScopesResponse{} },
|
||||||
|
"variables": func() Message { return &VariablesResponse{} },
|
||||||
|
"setVariable": func() Message { return &SetVariableResponse{} },
|
||||||
|
"source": func() Message { return &SourceResponse{} },
|
||||||
|
"threads": func() Message { return &ThreadsResponse{} },
|
||||||
|
"terminateThreads": func() Message { return &TerminateThreadsResponse{} },
|
||||||
|
"modules": func() Message { return &ModulesResponse{} },
|
||||||
|
"loadedSources": func() Message { return &LoadedSourcesResponse{} },
|
||||||
|
"evaluate": func() Message { return &EvaluateResponse{} },
|
||||||
|
"setExpression": func() Message { return &SetExpressionResponse{} },
|
||||||
|
"stepInTargets": func() Message { return &StepInTargetsResponse{} },
|
||||||
|
"gotoTargets": func() Message { return &GotoTargetsResponse{} },
|
||||||
|
"completions": func() Message { return &CompletionsResponse{} },
|
||||||
|
"exceptionInfo": func() Message { return &ExceptionInfoResponse{} },
|
||||||
|
"readMemory": func() Message { return &ReadMemoryResponse{} },
|
||||||
|
"disassemble": func() Message { return &DisassembleResponse{} },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 decodeEvent(data []byte) (Message, error) {
|
||||||
|
var e Event
|
||||||
|
if err := json.Unmarshal(data, &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ctor, ok := eventCtor[e.Event]; ok {
|
||||||
|
eventPtr := ctor()
|
||||||
|
err := json.Unmarshal(data, eventPtr)
|
||||||
|
return eventPtr, err
|
||||||
|
}
|
||||||
|
return nil, &DecodeProtocolMessageFieldError{"Event", "event", e.Event}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping of event ids and corresponding struct constructors that
|
||||||
|
// can be passed to json.Unmarshal.
|
||||||
|
var eventCtor = map[string]messageCtor{
|
||||||
|
"initialized": func() Message { return &InitializedEvent{} },
|
||||||
|
"stopped": func() Message { return &StoppedEvent{} },
|
||||||
|
"continued": func() Message { return &ContinuedEvent{} },
|
||||||
|
"exited": func() Message { return &ExitedEvent{} },
|
||||||
|
"terminated": func() Message { return &TerminatedEvent{} },
|
||||||
|
"thread": func() Message { return &ThreadEvent{} },
|
||||||
|
"output": func() Message { return &OutputEvent{} },
|
||||||
|
"breakpoint": func() Message { return &BreakpointEvent{} },
|
||||||
|
"module": func() Message { return &ModuleEvent{} },
|
||||||
|
"loadedSource": func() Message { return &LoadedSourceEvent{} },
|
||||||
|
"process": func() Message { return &ProcessEvent{} },
|
||||||
|
"capabilities": func() Message { return &CapabilitiesEvent{} },
|
||||||
|
}
|
||||||
3
vendor/github.com/google/go-dap/go.mod
generated
vendored
Normal file
3
vendor/github.com/google/go-dap/go.mod
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/google/go-dap
|
||||||
|
|
||||||
|
go 1.13
|
||||||
137
vendor/github.com/google/go-dap/io.go
generated
vendored
Normal file
137
vendor/github.com/google/go-dap/io.go
generated
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2019 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 int, 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.Atoi(headerAndLength[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
1589
vendor/github.com/google/go-dap/schematypes.go
generated
vendored
Normal file
1589
vendor/github.com/google/go-dap/schematypes.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user