delve/service/dap/server.go
ttoad 53998cbb18
pkg/proc,service/*: Supports sending output to clients when running programs remotely (#3253)
* wip: Support sending output when remote debug

* wip: Support local output and remote output

* wip: fix stderr and stdout assignment error

* wip: optimize code

* wip: Only if outputMode is "remote" is the redirected console output

* wip: Redirected debugMode output(Not tested on windows)

* wip: support remote debugging output redirection of windows

* wip: real-time write back output

* wip: support for windows

* wip: fix windows remote debug not output

* wip: fix truncated output redirection

* wip: delete printfln

* wip: use debugger.Config to pass redirect(macOS)

* wip: use debugger.Config to pass redirect(linux)

* wip: Change redirect to a concrete type

* wip: s.wg.wait before sending "terminated"

* wip: add proc/redirect test(darwin and linux)

* Merge branch 'master' of github.com:tttoad/delve into feat-console

* wip: Fix test failure on windows

* fix: undefined: proc.Redirects

* fix: compile failure

* wip: Remove useless code

* fix: filename error

* fix: os.file not close

* test: add server_test.redirect

* fix: Remove 'eol' from end of file

* fix: gdbserial: File not closed in file mode.
(in reality, gdbserial will never use file mode)

* feat: Remove "only-remote". Fix spelling mistakes.

* fix: spelling mistakes

* refactor: redirect

* fix: stdout and stderr are not set to default values

* fix: Restore code logic for rr.openRedirects()

* fix: Optimization Code

* fix: utiltest

* fix: execpt out

* fix: Resource release for redirects

* fix: build failure

* fix: clean->clear

* fix: build failure

* fix: test failure

* fix: Optimization Code

* style: remove useless code

* refactor: namedpipe

* refactor: namedpipe, launch ...

* fix: freebsd compile failure

* fix: proc_darwin compile failure

* style:  remove useless code

* feat: add d.config.Stdxx check on debug.Restart

* style: formatting and adding comments

* style: formatting and adding comments

* feat: add d.config.Stdxx check on debug.Restart

* style: namedpipe->redirector

* style: namedPipe->redirector

---------

Co-authored-by: 李翔 <qian.fu2@amh-group.com>
2023-07-05 08:39:01 -07:00

3889 lines
142 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.
package dap
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"go/constant"
"go/parser"
"io"
"math"
"net"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"runtime"
"runtime/debug"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/go-delve/delve/pkg/gobuild"
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/locspec"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/debugger"
"github.com/go-delve/delve/service/internal/sameuser"
"github.com/google/go-dap"
)
// Server implements a DAP server that can accept a single client for
// a single debug session (for now). It does not yet support restarting.
// That means that in addition to explicit shutdown requests,
// program termination and failed or closed client connection
// would also result in stopping this single-use server.
//
// The DAP server operates via the following goroutines:
//
// (1) Main goroutine where the server is created via NewServer(),
// started via Run() and stopped via Stop(). Once the server is
// started, this goroutine blocks until it receives a stop-server
// signal that can come from an OS interrupt (such as Ctrl-C) or
// config.DisconnectChan (passed to NewServer()) as a result of
// client connection failure or closure or a DAP disconnect request.
//
// (2) Run goroutine started from Run() that serves as both
// a listener and a client goroutine. It accepts a client connection,
// reads, decodes and dispatches each request from the client.
// For synchronous requests, it issues commands to the
// underlying debugger and sends back events and responses.
// These requests block while the debuggee is running, so,
// where applicable, the handlers need to check if debugging
// state is running, so there is a need for a halt request or
// a dummy/error response to avoid blocking.
//
// This is the only goroutine that sends a stop-server signal
// via config.DisconnectChan when encountering a client connection
// error or responding to a (synchronous) DAP disconnect request.
// Once stop is triggered, the goroutine exits.
//
// Unlike rpccommon, there is not another layer of per-client
// goroutines here because the dap server does not support
// multiple clients.
//
// (3) Per-request goroutine is started for each asynchronous request
// that resumes execution. We check if target is running already, so
// there should be no more than one pending asynchronous request at
// a time. This goroutine issues commands to the underlying debugger
// and sends back events and responses. It takes a setup-done channel
// as an argument and temporarily blocks the request loop until setup
// for asynchronous execution is complete and targe is running.
// Once done, it unblocks processing of parallel requests unblocks
// (e.g. disconnecting while the program is running).
//
// These per-request goroutines never send a stop-server signal.
// They block on running debugger commands that are interrupted
// when halt is issued while stopping. At that point these goroutines
// wrap-up and exit.
type Server struct {
// config is all the information necessary to start the debugger and server.
config *Config
// listener is used to accept the client connection.
// When working with a predetermined client, this is nil.
listener net.Listener
// session is the debug session that comes with an client connection.
session *Session
sessionMu sync.Mutex
}
// Session is an abstraction for serving and shutting down
// a DAP debug session with a pre-connected client.
// TODO(polina): move this to a different file/package
type Session struct {
config *Config
id int
// stackFrameHandles maps frames of each goroutine to unique ids across all goroutines.
// Reset at every stop.
stackFrameHandles *handlesMap
// variableHandles maps compound variables to unique references within their stack frame.
// Reset at every stop.
// See also comment for convertVariable.
variableHandles *variablesHandlesMap
// args tracks special settings for handling debug session requests.
args launchAttachArgs
// exceptionErr tracks the runtime error that last occurred.
exceptionErr error
// clientCapabilities tracks special settings for handling debug session requests.
clientCapabilities dapClientCapabilities
// mu synchronizes access to objects set on start-up (from run goroutine)
// and stopped on teardown (from main goroutine)
mu sync.Mutex
// conn is the accepted client connection.
conn *connection
// debugger is the underlying debugger service.
debugger *debugger.Debugger
// binaryToRemove is the temp compiled binary to be removed on disconnect (if any).
binaryToRemove string
// noDebugProcess is set for the noDebug launch process.
noDebugProcess *process
// sendingMu synchronizes writing to conn
// to ensure that messages do not get interleaved
sendingMu sync.Mutex
// runningCmd tracks whether the server is running an asynchronous
// command that resumes execution, which may not correspond to the actual
// running state of the process (e.g. if a command is temporarily interrupted).
runningCmd bool
runningMu sync.Mutex
// haltRequested tracks whether a halt of the program has been requested, which may
// not correspond to whether a Halt Request has been sent to the target.
haltRequested bool
haltMu sync.Mutex
// changeStateMu must be held for a request to protect itself from another goroutine
// changing the state of the running process at the same time.
changeStateMu sync.Mutex
// stdoutReader the programs's stdout.
stdoutReader io.ReadCloser
// stderrReader the program's stderr.
stderrReader io.ReadCloser
// preTerminatedWG the WaitGroup that needs to wait before sending a terminated event.
preTerminatedWG sync.WaitGroup
}
// Config is all the information needed to start the debugger, handle
// DAP connection traffic and signal to the server when it is time to stop.
type Config struct {
*service.Config
// log is used for structured logging.
log logflags.Logger
// StopTriggered is closed when the server is Stop()-ed.
// Can be used to safeguard against duplicate shutdown sequences.
StopTriggered chan struct{}
}
type connection struct {
io.ReadWriteCloser
closed chan struct{}
}
func (c *connection) Close() error {
select {
case <-c.closed:
default:
close(c.closed)
}
return c.ReadWriteCloser.Close()
}
func (c *connection) isClosed() bool {
select {
case <-c.closed:
return true
default:
return false
}
}
type process struct {
*exec.Cmd
exited chan struct{}
}
// launchAttachArgs captures arguments from launch/attach request that
// impact handling of subsequent requests.
// The fields with cfgName tag can be updated through an evaluation request.
type launchAttachArgs struct {
// stopOnEntry is set to automatically stop the debuggee after start.
stopOnEntry bool
// StackTraceDepth is the maximum length of the returned list of stack frames.
StackTraceDepth int `cfgName:"stackTraceDepth"`
// ShowGlobalVariables indicates if global package variables should be loaded.
ShowGlobalVariables bool `cfgName:"showGlobalVariables"`
// ShowRegisters indicates if register values should be loaded.
ShowRegisters bool `cfgName:"showRegisters"`
// GoroutineFilters are the filters used when loading goroutines.
GoroutineFilters string `cfgName:"goroutineFilters"`
// HideSystemGoroutines indicates if system goroutines should be removed from threads
// responses.
HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
// substitutePathClientToServer indicates rules for converting file paths between client and debugger.
substitutePathClientToServer [][2]string `cfgName:"substitutePath"`
// substitutePathServerToClient indicates rules for converting file paths between debugger and client.
substitutePathServerToClient [][2]string
}
// defaultArgs borrows the defaults for the arguments from the original vscode-go adapter.
// TODO(polinasok): clean up this and its reference (Server.args)
// in favor of default*Config variables defined in types.go.
var defaultArgs = launchAttachArgs{
stopOnEntry: false,
StackTraceDepth: 50,
ShowGlobalVariables: false,
HideSystemGoroutines: false,
ShowRegisters: false,
GoroutineFilters: "",
substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{},
}
// dapClientCapabilities captures arguments from initialize request that
// impact handling of subsequent requests.
type dapClientCapabilities struct {
supportsVariableType bool
supportsVariablePaging bool
supportsRunInTerminalRequest bool
supportsMemoryReferences bool
supportsProgressReporting bool
}
// DefaultLoadConfig controls how variables are loaded from the target's memory.
// These limits are conservative to minimize performance overhead for bulk loading.
// With dlv-dap, users do not have a way to adjust these.
// Instead we are focusing in interactive loading with nested reloads, array/map
// paging and context-specific string limits.
var DefaultLoadConfig = proc.LoadConfig{
FollowPointers: true,
MaxVariableRecurse: 1,
// TODO(polina): consider 1024 limit instead:
// - vscode+C appears to use 1024 as the load limit
// - vscode viewlet hover truncates at 1023 characters
MaxStringLen: 512,
MaxArrayValues: 64,
MaxStructFields: -1,
}
const (
// When a user examines a single string, we can relax the loading limit.
maxSingleStringLen = 4 << 10 // 4096
// Results of a call are single-use and transient. We need to maximize
// what is presented. A common use case of a call injection is to
// stringify complex data conveniently.
maxStringLenInCallRetVars = 1 << 10 // 1024
)
var (
// Max number of goroutines that we will return.
// This is a var for testing
maxGoroutines = 1 << 10
)
// NewServer creates a new DAP Server. It takes an opened Listener
// via config and assumes its ownership. config.DisconnectChan has to be set;
// it will be closed by the server when the client fails to connect,
// disconnects or requests shutdown. Once config.DisconnectChan is closed,
// Server.Stop() must be called to shutdown this single-user server.
//
// NewServer can be used to create a special DAP Server that works
// only with a predetermined client. In that case, config.Listener is
// nil and its RunWithClient must be used instead of Run.
func NewServer(config *service.Config) *Server {
logger := logflags.DAPLogger()
if config.Listener != nil {
logflags.WriteDAPListeningMessage(config.Listener.Addr())
} else {
logger.Debug("DAP server for a predetermined client")
}
logger.Debug("DAP server pid = ", os.Getpid())
if config.AcceptMulti {
logger.Warn("DAP server does not support accept-multiclient mode")
config.AcceptMulti = false
}
return &Server{
config: &Config{
Config: config,
log: logger,
StopTriggered: make(chan struct{}),
},
listener: config.Listener,
}
}
var sessionCount = 0
// NewSession creates a new client session that can handle DAP traffic.
// It takes an open connection and provides a Close() method to shut it
// down when the DAP session disconnects or a connection error occurs.
func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debugger) *Session {
sessionCount++
if config.log == nil {
config.log = logflags.DAPLogger()
}
config.log.Debugf("DAP connection %d started", sessionCount)
if config.StopTriggered == nil {
config.log.Fatal("Session must be configured with StopTriggered")
}
return &Session{
config: config,
id: sessionCount,
conn: &connection{conn, make(chan struct{})},
stackFrameHandles: newHandlesMap(),
variableHandles: newVariablesHandlesMap(),
args: defaultArgs,
exceptionErr: nil,
debugger: debugger,
}
}
// If user-specified options are provided via Launch/AttachRequest,
// we override the defaults for optional args.
func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) {
s.args.stopOnEntry = args.StopOnEntry
if depth := args.StackTraceDepth; depth > 0 {
s.args.StackTraceDepth = depth
}
s.args.ShowGlobalVariables = args.ShowGlobalVariables
s.args.ShowRegisters = args.ShowRegisters
s.args.HideSystemGoroutines = args.HideSystemGoroutines
s.args.GoroutineFilters = args.GoroutineFilters
if paths := args.SubstitutePath; len(paths) > 0 {
clientToServer := make([][2]string, 0, len(paths))
serverToClient := make([][2]string, 0, len(paths))
for _, p := range paths {
clientToServer = append(clientToServer, [2]string{p.From, p.To})
serverToClient = append(serverToClient, [2]string{p.To, p.From})
}
s.args.substitutePathClientToServer = clientToServer
s.args.substitutePathServerToClient = serverToClient
}
}
// 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 or stops the noDebug process.
// This method mustn't be called more than once.
// StopTriggered notifies other goroutines that stop is in progress.
func (s *Server) Stop() {
s.config.log.Debug("DAP server stopping...")
defer s.config.log.Debug("DAP server stopped")
close(s.config.StopTriggered)
if s.listener != nil {
// If run goroutine is blocked on accept, this will unblock it.
_ = s.listener.Close()
}
s.sessionMu.Lock()
defer s.sessionMu.Unlock()
if s.session == nil {
return
}
// If run goroutine is blocked on read, this will unblock it.
s.session.Close()
}
// Close closes the underlying debugger/process and connection.
// May be called more than once.
func (s *Session) Close() {
s.mu.Lock()
defer s.mu.Unlock()
if s.debugger != nil {
killProcess := s.debugger.AttachPid() == 0
s.stopDebugSession(killProcess)
} else if s.noDebugProcess != nil {
s.stopNoDebugProcess()
}
// The binary is no longer in use by the debugger. It is safe to remove it.
if s.binaryToRemove != "" {
gobuild.Remove(s.binaryToRemove)
s.binaryToRemove = "" // avoid error printed on duplicate removal
}
// Close client connection last, so other shutdown stages
// can send client notifications.
// Unless Stop() was called after read loop in ServeDAPCodec()
// returned, this will result in a closed connection error
// on next read, breaking out the read loop and
// allowing the run goroutines to exit.
// This connection is closed here and in serveDAPCodec().
// If this was a forced shutdown, external stop logic can close this first.
// If this was a client loop exit (on error or disconnect), serveDAPCodec()
// will be first.
// Duplicate close calls return an error, but are not fatal.
_ = s.conn.Close()
}
// triggerServerStop closes DisconnectChan if not nil, which
// signals that client sent a disconnect request or there was connection
// failure or closure. Since the server currently services only one
// client, this is used as a signal to stop the entire server.
// The function safeguards against 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.
func (c *Config) triggerServerStop() {
// Avoid accidentally closing the channel twice and causing a panic, when
// this function is called more than once because stop was triggered
// by multiple conditions simultaneously.
if c.DisconnectChan != nil {
close(c.DisconnectChan)
c.DisconnectChan = nil
}
// There should be no logic here after the stop-server
// signal that might cause everything to shutdown before this
// logic gets executed.
}
// 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 dap server only once? Note that some requests
// may change the server's environment (e.g. see dlvCwd of launch configuration).
// So if we want to reuse this server for multiple independent debugging sessions
// we need to take that into consideration.
func (s *Server) Run() {
if s.listener == nil {
s.config.log.Fatal("Misconfigured server: no Listener is configured.")
return
}
go func() {
conn, err := s.listener.Accept() // listener is closed in Stop()
if err != nil {
select {
case <-s.config.StopTriggered:
default:
s.config.log.Errorf("Error accepting client connection: %s\n", err)
s.config.triggerServerStop()
}
return
}
if s.config.CheckLocalConnUser {
if !sameuser.CanAccept(s.listener.Addr(), conn.LocalAddr(), conn.RemoteAddr()) {
s.config.log.Error("Error accepting client connection: Only connections from the same user that started this instance of Delve are allowed to connect. See --only-same-user.")
s.config.triggerServerStop()
return
}
}
s.runSession(conn)
}()
}
func (s *Server) runSession(conn io.ReadWriteCloser) {
s.sessionMu.Lock()
s.session = NewSession(conn, s.config, nil) // closed in Stop()
s.sessionMu.Unlock()
s.session.ServeDAPCodec()
}
// RunWithClient is similar to Run but works only with an already established
// connection instead of waiting on the listener to accept a new client.
// RunWithClient takes ownership of conn. Debugger won't be started
// until a launch/attach request is received over the connection.
func (s *Server) RunWithClient(conn net.Conn) {
if s.listener != nil {
s.config.log.Fatal("RunWithClient must not be used when the Server is configured with a Listener")
return
}
s.config.log.Debugf("Connected to the client at %s", conn.RemoteAddr())
go s.runSession(conn)
}
func (s *Session) address() string {
if s.config.Listener != nil {
return s.config.Listener.Addr().String()
}
if netconn, ok := s.conn.ReadWriteCloser.(net.Conn); ok {
return netconn.LocalAddr().String()
}
return ""
}
// ServeDAPCodec reads and decodes requests from the client
// until it encounters an error or EOF, when it sends
// a disconnect signal and returns.
func (s *Session) ServeDAPCodec() {
// Close conn, but not the debugger in case we are in AcceptMulti mode.
// If not, debugger will be shut down in Stop().
defer s.conn.Close()
reader := bufio.NewReader(s.conn)
for {
request, err := dap.ReadProtocolMessage(reader)
// Handle dap.DecodeProtocolMessageFieldError errors gracefully by responding with an ErrorResponse.
// For example:
// -- "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.
//
// Other errors, such as unmarshalling errors, will log the error and cause the server to trigger
// a stop.
if err != nil {
s.config.log.Debug("DAP error: ", err)
select {
case <-s.config.StopTriggered:
default:
if !s.config.AcceptMulti {
defer s.config.triggerServerStop()
}
if err != io.EOF { // EOF means client closed connection
if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
// Send an error response to the users if we were unable to process the message.
s.sendInternalErrorResponse(decodeErr.Seq, err.Error())
continue
}
s.config.log.Error("DAP error: ", err)
}
}
return
}
s.handleRequest(request)
if _, ok := request.(*dap.DisconnectRequest); ok {
// disconnect already shut things down and triggered stopping
return
}
}
}
// In case a handler panics, we catch the panic to avoid crashing both
// the server and the target. We send an error response back, but
// in case its a dup and ignored by the client, we also log the error.
func (s *Session) recoverPanic(request dap.Message) {
if ierr := recover(); ierr != nil {
s.config.log.Errorf("recovered panic: %s\n%s\n", ierr, debug.Stack())
s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("%v", ierr))
}
}
func (s *Session) handleRequest(request dap.Message) {
defer s.recoverPanic(request)
jsonmsg, _ := json.Marshal(request)
s.config.log.Debug("[<- from client]", string(jsonmsg))
if _, ok := request.(dap.RequestMessage); !ok {
s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("Unable to process non-request %#v\n", request))
return
}
if s.isNoDebug() {
switch request := request.(type) {
case *dap.DisconnectRequest:
s.onDisconnectRequest(request)
case *dap.RestartRequest:
s.sendUnsupportedErrorResponse(request.Request)
default:
r := request.(dap.RequestMessage).GetRequest()
s.sendErrorResponse(*r, NoDebugIsRunning, "noDebug mode", fmt.Sprintf("unable to process '%s' request", r.Command))
}
return
}
// These requests, can be handled regardless of whether the targret is running
switch request := request.(type) {
case *dap.InitializeRequest: // Required
s.onInitializeRequest(request)
return
case *dap.LaunchRequest: // Required
s.onLaunchRequest(request)
return
case *dap.AttachRequest: // Required
s.onAttachRequest(request)
return
case *dap.DisconnectRequest: // Required
s.onDisconnectRequest(request)
return
case *dap.PauseRequest: // Required
s.onPauseRequest(request)
return
case *dap.TerminateRequest: // Optional (capability supportsTerminateRequest)
/*TODO*/ s.onTerminateRequest(request) // not yet implemented
return
case *dap.RestartRequest: // Optional (capability supportsRestartRequest)
/*TODO*/ s.onRestartRequest(request) // not yet implemented
return
}
// Most requests cannot be processed while the debuggee is running.
// We have a couple of options for handling these without blocking
// the request loop indefinitely when we are in running state.
// --1-- Return a dummy response or an error right away.
// --2-- Halt execution, process the request, maybe resume execution.
// --3-- Handle such requests asynchronously and let them block until
// the process stops or terminates (e.g. using a channel and a single
// goroutine to preserve the order). This might not be appropriate
// for requests such as continue or step because they would skip
// the stop, resuming execution right away. Other requests
// might not be relevant anymore when the stop is finally reached, and
// state changed from the previous snapshot. The user might want to
// resume execution before the backlog of buffered requests is cleared,
// so we would have to either cancel them or delay processing until
// the next stop. In addition, the editor itself might block waiting
// for these requests to return. We are not aware of any requests
// that would benefit from this approach at this time.
if s.debugger != nil && s.debugger.IsRunning() || s.isRunningCmd() {
switch request := request.(type) {
case *dap.ThreadsRequest: // Required
// On start-up, the client requests the baseline of currently existing threads
// right away as there are a number of DAP requests that require a thread id
// (pause, continue, stacktrace, etc). This can happen after the program
// continues on entry, preventing the client from handling any pause requests
// from the user. We remedy this by sending back a placeholder thread id
// for the current goroutine.
response := &dap.ThreadsResponse{
Response: *newResponse(request.Request),
Body: dap.ThreadsResponseBody{Threads: []dap.Thread{{Id: -1, Name: "Current"}}},
}
s.send(response)
case *dap.SetBreakpointsRequest: // Required
s.changeStateMu.Lock()
defer s.changeStateMu.Unlock()
s.config.log.Debug("halting execution to set breakpoints")
_, err := s.halt()
if err != nil {
s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
return
}
s.onSetBreakpointsRequest(request)
case *dap.SetFunctionBreakpointsRequest: // Optional (capability supportsFunctionBreakpoints)
s.changeStateMu.Lock()
defer s.changeStateMu.Unlock()
s.config.log.Debug("halting execution to set breakpoints")
_, err := s.halt()
if err != nil {
s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", err.Error())
return
}
s.onSetFunctionBreakpointsRequest(request)
default:
r := request.(dap.RequestMessage).GetRequest()
s.sendErrorResponse(*r, DebuggeeIsRunning, fmt.Sprintf("Unable to process `%s`", r.Command), "debuggee is running")
}
return
}
// Requests below can only be handled while target is stopped.
// Some of them are blocking and will be handled synchronously
// on this goroutine while non-blocking requests will be dispatched
// to another goroutine. Please note that because of the running
// check above, there should be no more than one pending asynchronous
// request at a time.
// Non-blocking request handlers will signal when they are ready
// setting up for async execution, so more requests can be processed.
resumeRequestLoop := make(chan struct{})
switch request := request.(type) {
//--- Asynchronous requests ---
case *dap.ConfigurationDoneRequest: // Optional (capability supportsConfigurationDoneRequest)
go func() {
defer s.recoverPanic(request)
s.onConfigurationDoneRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.ContinueRequest: // Required
go func() {
defer s.recoverPanic(request)
s.onContinueRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.NextRequest: // Required
go func() {
defer s.recoverPanic(request)
s.onNextRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.StepInRequest: // Required
go func() {
defer s.recoverPanic(request)
s.onStepInRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.StepOutRequest: // Required
go func() {
defer s.recoverPanic(request)
s.onStepOutRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.StepBackRequest: // Optional (capability supportsStepBack)
go func() {
defer s.recoverPanic(request)
s.onStepBackRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
case *dap.ReverseContinueRequest: // Optional (capability supportsStepBack)
go func() {
defer s.recoverPanic(request)
s.onReverseContinueRequest(request, resumeRequestLoop)
}()
<-resumeRequestLoop
//--- Synchronous requests ---
case *dap.SetBreakpointsRequest: // Required
s.onSetBreakpointsRequest(request)
case *dap.SetFunctionBreakpointsRequest: // Optional (capability supportsFunctionBreakpoints)
s.onSetFunctionBreakpointsRequest(request)
case *dap.SetInstructionBreakpointsRequest: // Optional (capability 'supportsInstructionBreakpoints')
s.onSetInstructionBreakpointsRequest(request)
case *dap.SetExceptionBreakpointsRequest: // Optional (capability exceptionBreakpointFilters)
s.onSetExceptionBreakpointsRequest(request)
case *dap.ThreadsRequest: // Required
s.onThreadsRequest(request)
case *dap.StackTraceRequest: // Required
s.onStackTraceRequest(request)
case *dap.ScopesRequest: // Required
s.onScopesRequest(request)
case *dap.VariablesRequest: // Required
s.onVariablesRequest(request)
case *dap.EvaluateRequest: // Required
s.onEvaluateRequest(request)
case *dap.SetVariableRequest: // Optional (capability supportsSetVariable)
s.onSetVariableRequest(request)
case *dap.ExceptionInfoRequest: // Optional (capability supportsExceptionInfoRequest)
s.onExceptionInfoRequest(request)
case *dap.DisassembleRequest: // Optional (capability supportsDisassembleRequest)
s.onDisassembleRequest(request)
//--- Requests that we may want to support ---
case *dap.SourceRequest: // Required
/*TODO*/ s.sendUnsupportedErrorResponse(request.Request) // https://github.com/go-delve/delve/issues/2851
case *dap.SetExpressionRequest: // Optional (capability supportsSetExpression)
/*TODO*/ s.onSetExpressionRequest(request) // Not yet implemented
case *dap.LoadedSourcesRequest: // Optional (capability supportsLoadedSourcesRequest)
/*TODO*/ s.onLoadedSourcesRequest(request) // Not yet implemented
case *dap.ReadMemoryRequest: // Optional (capability supportsReadMemoryRequest)
/*TODO*/ s.onReadMemoryRequest(request) // Not yet implemented
case *dap.CancelRequest: // Optional (capability supportsCancelRequest)
/*TODO*/ s.onCancelRequest(request) // Not yet implemented (does this make sense?)
case *dap.ModulesRequest: // Optional (capability supportsModulesRequest)
/*TODO*/ s.sendUnsupportedErrorResponse(request.Request) // Not yet implemented (does this make sense?)
//--- Requests that we do not plan to support ---
case *dap.RestartFrameRequest: // Optional (capability supportsRestartFrame)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.GotoRequest: // Optional (capability supportsGotoTargetsRequest)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.TerminateThreadsRequest: // Optional (capability supportsTerminateThreadsRequest)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.StepInTargetsRequest: // Optional (capability supportsStepInTargetsRequest)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.GotoTargetsRequest: // Optional (capability supportsGotoTargetsRequest)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.CompletionsRequest: // Optional (capability supportsCompletionsRequest)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.DataBreakpointInfoRequest: // Optional (capability supportsDataBreakpoints)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.SetDataBreakpointsRequest: // Optional (capability supportsDataBreakpoints)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.BreakpointLocationsRequest: // Optional (capability supportsBreakpointLocationsRequest)
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.
s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("Unable to process %#v\n", request))
}
}
func (s *Session) send(message dap.Message) {
jsonmsg, _ := json.Marshal(message)
s.config.log.Debug("[-> to client]", string(jsonmsg))
// TODO(polina): consider using a channel for all the sends and to have a dedicated
// goroutine that reads from that channel and sends over the connection.
// This will avoid blocking on slow network sends.
s.sendingMu.Lock()
defer s.sendingMu.Unlock()
err := dap.WriteProtocolMessage(s.conn, message)
if err != nil {
s.config.log.Debug(err)
}
}
func (s *Session) logToConsole(msg string) {
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: msg + "\n",
Category: "console",
}})
}
func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
s.setClientCapabilities(request.Arguments)
if request.Arguments.PathFormat != "path" {
s.sendErrorResponse(request.Request, FailedToInitialize, "Failed to initialize",
fmt.Sprintf("Unsupported 'pathFormat' value '%s'.", request.Arguments.PathFormat))
return
}
if !request.Arguments.LinesStartAt1 {
s.sendErrorResponse(request.Request, FailedToInitialize, "Failed to initialize",
"Only 1-based line numbers are supported.")
return
}
if !request.Arguments.ColumnsStartAt1 {
s.sendErrorResponse(request.Request, FailedToInitialize, "Failed to initialize",
"Only 1-based column numbers are supported.")
return
}
// TODO(polina): Respond with an error if debug session started
// with an initialize request is in progress?
response := &dap.InitializeResponse{Response: *newResponse(request.Request)}
response.Body.SupportsConfigurationDoneRequest = true
response.Body.SupportsConditionalBreakpoints = true
response.Body.SupportsDelayedStackTraceLoading = true
response.Body.SupportsFunctionBreakpoints = true
response.Body.SupportsInstructionBreakpoints = true
response.Body.SupportsExceptionInfoRequest = true
response.Body.SupportsSetVariable = true
response.Body.SupportsEvaluateForHovers = true
response.Body.SupportsClipboardContext = true
response.Body.SupportsSteppingGranularity = true
response.Body.SupportsLogPoints = true
response.Body.SupportsDisassembleRequest = true
// To be enabled by CapabilitiesEvent based on launch configuration
response.Body.SupportsStepBack = false
response.Body.SupportTerminateDebuggee = false
// TODO(polina): support these requests in addition to vscode-go feature parity
response.Body.SupportsTerminateRequest = false
response.Body.SupportsRestartRequest = false
response.Body.SupportsSetExpression = false
response.Body.SupportsLoadedSourcesRequest = false
response.Body.SupportsReadMemoryRequest = false
response.Body.SupportsCancelRequest = false
s.send(response)
}
func (s *Session) setClientCapabilities(args dap.InitializeRequestArguments) {
s.clientCapabilities.supportsMemoryReferences = args.SupportsMemoryReferences
s.clientCapabilities.supportsProgressReporting = args.SupportsProgressReporting
s.clientCapabilities.supportsRunInTerminalRequest = args.SupportsRunInTerminalRequest
s.clientCapabilities.supportsVariablePaging = args.SupportsVariablePaging
s.clientCapabilities.supportsVariableType = args.SupportsVariableType
}
func cleanExeName(name string) string {
if runtime.GOOS == "windows" && filepath.Ext(name) != ".exe" {
return name + ".exe"
}
return name
}
func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
var err error
if s.debugger != nil {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
fmt.Sprintf("debug session already in progress at %s - use remote attach mode to connect to a server with an active debug session", s.address()))
return
}
var args = defaultLaunchConfig // narrow copy for initializing non-zero default values
if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
s.sendShowUserErrorResponse(request.Request,
FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - %v", err))
return
}
s.config.log.Debug("parsed launch config: ", prettyPrint(args))
if args.DlvCwd != "" {
if err := os.Chdir(args.DlvCwd); err != nil {
s.sendShowUserErrorResponse(request.Request,
FailedToLaunch, "Failed to launch", fmt.Sprintf("failed to chdir to %q - %v", args.DlvCwd, err))
return
}
}
for k, v := range args.Env {
if v != nil {
if err := os.Setenv(k, *v); err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("failed to setenv(%v) - %v", k, err))
return
}
} else {
if err := os.Unsetenv(k); err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("failed to unsetenv(%v) - %v", k, err))
return
}
}
}
if args.Mode == "" {
args.Mode = "debug"
}
if !isValidLaunchMode(args.Mode) {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", args.Mode))
return
}
if args.Program == "" && args.Mode != "replay" { // Only fail on modes requiring a program
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
"The program attribute is missing in debug configuration.")
return
}
if args.Backend == "" {
args.Backend = "default"
}
if args.Mode == "replay" {
// Validate trace directory
if args.TraceDirPath == "" {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
"The 'traceDirPath' attribute is missing in debug configuration.")
return
}
// Assign the rr trace directory path to debugger configuration
s.config.Debugger.CoreFile = args.TraceDirPath
args.Backend = "rr"
}
if args.Mode == "core" {
// Validate core dump path
if args.CoreFilePath == "" {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
"The 'coreFilePath' attribute is missing in debug configuration.")
return
}
// Assign the non-empty core file path to debugger configuration. This will
// trigger a native core file replay instead of an rr trace replay
s.config.Debugger.CoreFile = args.CoreFilePath
args.Backend = "core"
}
s.config.Debugger.Backend = args.Backend
// Prepare the debug executable filename, building it if necessary
debugbinary := args.Program
if args.Mode == "debug" || args.Mode == "test" {
deleteOnError := false
if args.Output == "" {
deleteOnError = true
args.Output = gobuild.DefaultDebugBinaryPath("__debug_bin")
} else {
args.Output = cleanExeName(args.Output)
}
args.Output, err = filepath.Abs(args.Output)
if err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
return
}
debugbinary = args.Output
var cmd string
var out []byte
switch args.Mode {
case "debug":
cmd, out, err = gobuild.GoBuildCombinedOutput(args.Output, []string{args.Program}, args.BuildFlags)
case "test":
cmd, out, err = gobuild.GoTestBuildCombinedOutput(args.Output, []string{args.Program}, args.BuildFlags)
}
args.DlvCwd, _ = filepath.Abs(args.DlvCwd)
s.config.log.Debugf("building from %q: [%s]", args.DlvCwd, cmd)
if err != nil {
if deleteOnError {
gobuild.Remove(args.Output)
}
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: fmt.Sprintf("Build Error: %s\n%s (%s)\n", cmd, strings.TrimSpace(string(out)), err.Error()),
Category: "stderr",
}})
// Users are used to checking the Debug Console for build errors.
// No need to bother them with a visible pop-up.
s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
"Build error: Check the debug console for details.")
return
}
s.mu.Lock()
s.binaryToRemove = args.Output
s.mu.Unlock()
}
s.config.ProcessArgs = append([]string{debugbinary}, args.Args...)
s.setLaunchAttachArgs(args.LaunchAttachCommonConfig)
if args.Cwd == "" {
if args.Mode == "test" {
// In test mode, run the test binary from the package directory
// like in `go test` and `dlv test` by default.
args.Cwd = s.getPackageDir(args.Program)
} else {
args.Cwd = "."
}
}
s.config.Debugger.WorkingDir = args.Cwd
// Backend layers will interpret paths relative to server's working directory:
// reflect that before logging.
argsToLog := args
argsToLog.Program, _ = filepath.Abs(args.Program)
argsToLog.Cwd, _ = filepath.Abs(args.Cwd)
s.config.log.Debugf("launching binary '%s' with config: %s", debugbinary, prettyPrint(argsToLog))
var redirected = false
switch args.OutputMode {
case "remote":
redirected = true
case "local", "":
// noting
default:
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
fmt.Sprintf("invalid debug configuration - unsupported 'outputMode' attribute %q", args.OutputMode))
return
}
redirectedFunc := func(stdoutReader io.ReadCloser, stderrReader io.ReadCloser) {
runReadFunc := func(reader io.ReadCloser, category string) {
defer s.preTerminatedWG.Done()
defer reader.Close()
// Read output from `reader` and send to client
var out [1024]byte
for {
n, err := reader.Read(out[:])
if err != nil {
if errors.Is(io.EOF, err) {
return
}
s.config.log.Errorf("failed read by %s - %v ", category, err)
return
}
outs := string(out[:n])
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: outs,
Category: category,
}})
}
}
s.preTerminatedWG.Add(2)
go runReadFunc(stdoutReader, "stdout")
go runReadFunc(stderrReader, "stderr")
}
if args.NoDebug {
s.mu.Lock()
cmd, err := s.newNoDebugProcess(debugbinary, args.Args, s.config.Debugger.WorkingDir, redirected)
s.mu.Unlock()
if err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
return
}
// Skip 'initialized' event, which will prevent the client from sending
// debug-related requests.
s.send(&dap.LaunchResponse{Response: *newResponse(request.Request)})
// Start the program on a different goroutine, so we can listen for disconnect request.
go func() {
if redirected {
redirectedFunc(s.stdoutReader, s.stderrReader)
}
if err := cmd.Wait(); err != nil {
s.config.log.Debugf("program exited with error: %v", err)
}
close(s.noDebugProcess.exited)
s.logToConsole(proc.ErrProcessExited{Pid: cmd.ProcessState.Pid(), Status: cmd.ProcessState.ExitCode()}.Error())
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
}()
return
}
var clear func()
if redirected {
var (
readers [2]io.ReadCloser
outputRedirects [2]proc.OutputRedirect
)
for i := 0; i < 2; i++ {
readers[i], outputRedirects[i], err = proc.Redirector()
if err != nil {
s.sendShowUserErrorResponse(request.Request, InternalError, "Internal Error",
fmt.Sprintf("failed to generate stdio pipes - %v", err))
return
}
}
s.config.Debugger.Stdout = outputRedirects[0]
s.config.Debugger.Stderr = outputRedirects[1]
redirectedFunc(readers[0], readers[1])
clear = func() {
for index := range readers {
if closeErr := readers[index].Close(); closeErr != nil {
s.config.log.Warnf("failed to clear redirects - %v", closeErr)
}
}
}
}
func() {
s.mu.Lock()
defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error
s.debugger, err = debugger.New(&s.config.Debugger, s.config.ProcessArgs)
}()
if err != nil {
if s.binaryToRemove != "" {
gobuild.Remove(s.binaryToRemove)
}
s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
if redirected {
clear()
}
return
}
// Enable StepBack controls on supported backends
if s.config.Debugger.Backend == "rr" {
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportsStepBack: true}}})
}
// 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)})
}
func (s *Session) getPackageDir(pkg string) string {
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", pkg)
out, err := cmd.Output()
if err != nil {
s.config.log.Debugf("failed to determine package directory for %v: %v\n%s", pkg, err, out)
return "."
}
return string(bytes.TrimSpace(out))
}
// newNoDebugProcess is called from onLaunchRequest (run goroutine) and
// requires holding mu lock. It prepares process exec.Cmd to be started.
func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd string, redirected bool) (cmd *exec.Cmd, err error) {
if s.noDebugProcess != nil {
return nil, fmt.Errorf("another launch request is in progress")
}
cmd = exec.Command(program, targetArgs...)
cmd.Stdin, cmd.Dir = os.Stdin, wd
if redirected {
if s.stderrReader, err = cmd.StderrPipe(); err != nil {
return nil, err
}
if s.stdoutReader, err = cmd.StdoutPipe(); err != nil {
return nil, err
}
} else {
cmd.Stdout, cmd.Stderr = os.Stdin, os.Stderr
}
if err = cmd.Start(); err != nil {
return nil, err
}
s.noDebugProcess = &process{Cmd: cmd, exited: make(chan struct{})}
return cmd, nil
}
// stopNoDebugProcess is called from Stop (main goroutine) and
// onDisconnectRequest (run goroutine) and requires holding mu lock.
func (s *Session) stopNoDebugProcess() {
if s.noDebugProcess == nil {
// We already handled termination or there was never a process
return
}
select {
case <-s.noDebugProcess.exited:
s.noDebugProcess = nil
return
default:
}
// TODO(hyangah): gracefully terminate the process and its children processes.
s.logToConsole(fmt.Sprintf("Terminating process %d", s.noDebugProcess.Process.Pid))
s.noDebugProcess.Process.Kill() // Don't check error. Process killing and self-termination may race.
// Wait for kill to complete or time out
select {
case <-time.After(5 * time.Second):
s.config.log.Debug("noDebug process kill timed out")
case <-s.noDebugProcess.exited:
s.config.log.Debug("noDebug process killed")
s.noDebugProcess = nil
}
}
// 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 *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
s.mu.Lock()
defer s.mu.Unlock()
if s.debugger != nil && s.config.AcceptMulti && (request.Arguments == nil || !request.Arguments.TerminateDebuggee) {
// This is a multi-use server/debugger, so a disconnect request that doesn't
// terminate the debuggee should clean up only the client connection and pointer to debugger,
// but not the entire server.
status := "halted"
if s.isRunningCmd() {
status = "running"
} else if state, err := s.debugger.State(false); processExited(state, err) {
status = "exited"
s.preTerminatedWG.Wait()
}
s.logToConsole(fmt.Sprintf("Closing client session, but leaving multi-client DAP server at %s with debuggee %s", s.config.Listener.Addr().String(), status))
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
s.conn.Close()
s.debugger = nil
// The target is left in whatever state it is already in - halted or running.
// The users therefore have the flexibility to choose the appropriate state
// for their case before disconnecting. This is also desirable in case of
// the client connection fails unexpectedly and the user needs to reconnect.
// TODO(polina): should we always issue a continue here if it is not running
// like is done in vscode-go legacy adapter?
// Ideally we want to use bool suspendDebuggee flag, but it is not yet
// available in vscode: https://github.com/microsoft/vscode/issues/134412
return
}
defer s.config.triggerServerStop()
var err error
if s.debugger != nil {
// We always kill launched programs.
// In case of attach, we leave the program
// running by default, which can be
// overridden by an explicit request to terminate.
killProcess := s.debugger.AttachPid() == 0 || (request.Arguments != nil && request.Arguments.TerminateDebuggee)
err = s.stopDebugSession(killProcess)
} else if s.noDebugProcess != nil {
s.stopNoDebugProcess()
}
if err != nil {
s.sendErrorResponse(request.Request, DisconnectError, "Error while disconnecting", err.Error())
} else {
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
}
s.preTerminatedWG.Wait()
// The debugging session has ended, so we send a terminated event.
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
}
// stopDebugSession is called from Stop (main goroutine) and
// onDisconnectRequest (run goroutine) and requires holding mu lock.
// Returns any detach error other than proc.ErrProcessExited.
func (s *Session) stopDebugSession(killProcess bool) error {
s.changeStateMu.Lock()
defer func() {
// Avoid running stop sequence twice.
// It's not fatal, but will result in duplicate logging.
s.debugger = nil
s.changeStateMu.Unlock()
}()
if s.debugger == nil {
return nil
}
var err error
var exited error
// Halting will stop any debugger command that's pending on another
// per-request goroutine. Tell auto-resumer not to resume, so the
// goroutine can wrap-up and exit.
s.setHaltRequested(true)
state, err := s.halt()
if err == proc.ErrProcessDetached {
s.config.log.Debug("halt returned error: ", err)
return nil
}
if err != nil {
switch err.(type) {
case proc.ErrProcessExited:
exited = err
default:
s.config.log.Error("halt returned error: ", err)
if err.Error() == "no such process" {
exited = err
}
}
} else if state.Exited {
exited = proc.ErrProcessExited{Pid: s.debugger.ProcessPid(), Status: state.ExitStatus}
s.config.log.Debug("halt returned state: ", exited)
}
if exited != nil {
// TODO(suzmue): log exited error when the process exits, which may have been before
// halt was called.
s.logToConsole(exited.Error())
s.logToConsole("Detaching")
} else if killProcess {
s.logToConsole("Detaching and terminating target process")
} else {
s.logToConsole("Detaching without terminating target process")
}
err = s.debugger.Detach(killProcess)
if err != nil {
switch err.(type) {
case proc.ErrProcessExited:
s.config.log.Debug(err)
s.logToConsole(exited.Error())
err = nil
default:
s.config.log.Error("detach returned error: ", err)
}
}
return err
}
// halt sends a halt request if the debuggee is running.
// changeStateMu should be held when calling (*Server).halt.
func (s *Session) halt() (*api.DebuggerState, error) {
s.config.log.Debug("halting")
// Only send a halt request if the debuggee is running.
if s.debugger.IsRunning() {
return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
}
s.config.log.Debug("process not running")
return s.debugger.State(false)
}
func (s *Session) isNoDebug() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.noDebugProcess != nil
}
func (s *Session) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
if request.Arguments.Source.Path == "" {
s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", "empty file path")
return
}
clientPath := request.Arguments.Source.Path
serverPath := s.toServerPath(clientPath)
// Get all existing breakpoints that match for this source.
sourceRequestPrefix := fmt.Sprintf("sourceBp Path=%q ", request.Arguments.Source.Path)
breakpoints := s.setBreakpoints(sourceRequestPrefix, len(request.Arguments.Breakpoints), func(i int) *bpMetadata {
want := request.Arguments.Breakpoints[i]
return &bpMetadata{
name: fmt.Sprintf("%s Line=%d Column=%d", sourceRequestPrefix, want.Line, want.Column),
condition: want.Condition,
hitCondition: want.HitCondition,
logMessage: want.LogMessage,
}
}, func(i int) (*bpLocation, error) {
want := request.Arguments.Breakpoints[i]
return &bpLocation{
file: serverPath,
line: want.Line,
}, nil
})
response := &dap.SetBreakpointsResponse{Response: *newResponse(request.Request)}
response.Body.Breakpoints = breakpoints
s.send(response)
}
type bpMetadata struct {
name string
condition string
hitCondition string
logMessage string
}
type bpLocation struct {
file string
line int
addr uint64
addrs []uint64
}
// setBreakpoints is a helper function for setting source, function and instruction
// breakpoints. It takes the prefix of the name for all breakpoints that should be
// included, the total number of breakpoints, and functions for computing the metadata
// and the location. The location is computed separately because this may be more
// expensive to compute and may not always be necessary.
func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(i int) *bpMetadata, locFunc func(i int) (*bpLocation, error)) []dap.Breakpoint {
// If a breakpoint:
// -- exists and not in request => ClearBreakpoint
// -- exists and in request => AmendBreakpoint
// -- doesn't exist and in request => SetBreakpoint
// Get all existing breakpoints matching the prefix.
existingBps := s.getMatchingBreakpoints(prefix)
// createdBps is a set of breakpoint names that have been added
// during this request. This is used to catch duplicate set
// breakpoints requests and to track which breakpoints need to
// be deleted.
createdBps := make(map[string]struct{}, len(existingBps))
breakpoints := make([]dap.Breakpoint, totalBps)
// Amend existing breakpoints.
for i := 0; i < totalBps; i++ {
want := metadataFunc(i)
got, ok := existingBps[want.name]
if got == nil || !ok {
// Skip if the breakpoint does not already exist.
continue
}
var err error
if _, ok := createdBps[want.name]; ok {
err = fmt.Errorf("breakpoint already exists")
} else {
got.Disabled = false
got.Cond = want.condition
got.HitCond = want.hitCondition
err = setLogMessage(got, want.logMessage)
if err == nil {
err = s.debugger.AmendBreakpoint(got)
}
}
createdBps[want.name] = struct{}{}
s.updateBreakpointsResponse(breakpoints, i, err, got)
}
// Clear breakpoints.
// Any breakpoint that existed before this request but was not amended must be deleted.
s.clearBreakpoints(existingBps, createdBps)
// Add new breakpoints.
for i := 0; i < totalBps; i++ {
want := metadataFunc(i)
if _, ok := existingBps[want.name]; ok {
continue
}
var got *api.Breakpoint
wantLoc, err := locFunc(i)
if err == nil {
if _, ok := createdBps[want.name]; ok {
err = fmt.Errorf("breakpoint already exists")
} else {
bp := &api.Breakpoint{
Name: want.name,
File: wantLoc.file,
Line: wantLoc.line,
Addr: wantLoc.addr,
Addrs: wantLoc.addrs,
Cond: want.condition,
HitCond: want.hitCondition,
}
err = setLogMessage(bp, want.logMessage)
if err == nil {
// Create new breakpoints.
got, err = s.debugger.CreateBreakpoint(bp, "", nil, false)
}
}
}
createdBps[want.name] = struct{}{}
s.updateBreakpointsResponse(breakpoints, i, err, got)
}
return breakpoints
}
func setLogMessage(bp *api.Breakpoint, msg string) error {
tracepoint, userdata, err := parseLogPoint(msg)
if err != nil {
return err
}
bp.Tracepoint = tracepoint
if userdata != nil {
bp.UserData = *userdata
}
return nil
}
func (s *Session) updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, got *api.Breakpoint) {
breakpoints[i].Verified = (err == nil)
if err != nil {
breakpoints[i].Message = err.Error()
} else {
path := s.toClientPath(got.File)
breakpoints[i].Id = got.ID
breakpoints[i].Line = got.Line
breakpoints[i].Source = &dap.Source{Name: filepath.Base(path), Path: path}
}
}
// functionBpPrefix is the prefix of bp.Name for every breakpoint bp set
// in this request.
const functionBpPrefix = "functionBreakpoint"
func (s *Session) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpointsRequest) {
breakpoints := s.setBreakpoints(functionBpPrefix, len(request.Arguments.Breakpoints), func(i int) *bpMetadata {
want := request.Arguments.Breakpoints[i]
return &bpMetadata{
name: fmt.Sprintf("%s Name=%s", functionBpPrefix, want.Name),
condition: want.Condition,
hitCondition: want.HitCondition,
logMessage: "",
}
}, func(i int) (*bpLocation, error) {
want := request.Arguments.Breakpoints[i]
// Set the function breakpoint
spec, err := locspec.Parse(want.Name)
if err != nil {
return nil, err
}
if loc, ok := spec.(*locspec.NormalLocationSpec); !ok || loc.FuncBase == nil {
// Other locations do not make sense in the context of function breakpoints.
// Regex locations are likely to resolve to multiple places and offset locations
// are only meaningful at the time the breakpoint was created.
return nil, fmt.Errorf("breakpoint name %q could not be parsed as a function. name must be in the format 'funcName', 'funcName:line' or 'fileName:line'", want.Name)
}
if want.Name[0] == '.' {
return nil, fmt.Errorf("breakpoint names that are relative paths are not supported")
}
// Find the location of the function name. CreateBreakpoint requires the name to include the base
// (e.g. main.functionName is supported but not functionName).
// We first find the location of the function, and then set breakpoints for that location.
var locs []api.Location
locs, err = s.debugger.FindLocationSpec(-1, 0, 0, want.Name, spec, true, s.args.substitutePathClientToServer)
if err != nil {
return nil, err
}
if len(locs) == 0 {
return nil, err
}
if len(locs) > 0 {
s.config.log.Debugf("multiple locations found for %s", want.Name)
}
// Set breakpoint using the PCs that were found.
loc := locs[0]
return &bpLocation{addr: loc.PC, addrs: loc.PCs}, nil
})
response := &dap.SetFunctionBreakpointsResponse{Response: *newResponse(request.Request)}
response.Body.Breakpoints = breakpoints
s.send(response)
}
const instructionBpPrefix = "instructionBreakpoint"
func (s *Session) onSetInstructionBreakpointsRequest(request *dap.SetInstructionBreakpointsRequest) {
breakpoints := s.setBreakpoints(instructionBpPrefix, len(request.Arguments.Breakpoints), func(i int) *bpMetadata {
want := request.Arguments.Breakpoints[i]
return &bpMetadata{
name: fmt.Sprintf("%s PC=%s", instructionBpPrefix, want.InstructionReference),
condition: want.Condition,
hitCondition: want.HitCondition,
logMessage: "",
}
}, func(i int) (*bpLocation, error) {
want := request.Arguments.Breakpoints[i]
addr, err := strconv.ParseInt(want.InstructionReference, 0, 64)
if err != nil {
return nil, err
}
return &bpLocation{addr: uint64(addr)}, nil
})
response := &dap.SetInstructionBreakpointsResponse{Response: *newResponse(request.Request)}
response.Body.Breakpoints = breakpoints
s.send(response)
}
func (s *Session) clearBreakpoints(existingBps map[string]*api.Breakpoint, amendedBps map[string]struct{}) error {
for req, bp := range existingBps {
if _, ok := amendedBps[req]; ok {
continue
}
_, err := s.debugger.ClearBreakpoint(bp)
if err != nil {
return err
}
}
return nil
}
func (s *Session) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint {
existing := s.debugger.Breakpoints(false)
matchingBps := make(map[string]*api.Breakpoint, len(existing))
for _, bp := range existing {
// Skip special breakpoints such as for panic.
if bp.ID < 0 {
continue
}
// Skip breakpoints that do not meet the condition.
if !strings.HasPrefix(bp.Name, prefix) {
continue
}
matchingBps[bp.Name] = bp
}
return matchingBps
}
func (s *Session) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) {
// Unlike what DAP documentation claims, this request is always sent
// even though we specified no filters at initialization. Handle as no-op.
s.send(&dap.SetExceptionBreakpointsResponse{Response: *newResponse(request.Request)})
}
func closeIfOpen(ch chan struct{}) {
if ch != nil {
select {
case <-ch:
// already closed
default:
close(ch)
}
}
}
// onConfigurationDoneRequest handles 'configurationDone' request.
// This is an optional request enabled by capability supportsConfigurationDoneRequest.
// It gets triggered after all the debug requests that follow initialized event,
// so the s.debugger is guaranteed to be set. Expects the target to be halted.
func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange chan struct{}) {
defer closeIfOpen(allowNextStateChange)
if s.args.stopOnEntry {
e := &dap.StoppedEvent{
Event: *newEvent("stopped"),
Body: dap.StoppedEventBody{Reason: "entry", ThreadId: 1, AllThreadsStopped: true},
}
s.send(e)
}
s.debugger.TargetGroup().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints
s.logToConsole("Type 'dlv help' for list of commands.")
s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)})
if !s.args.stopOnEntry {
s.runUntilStopAndNotify(api.Continue, allowNextStateChange)
}
}
// onContinueRequest handles 'continue' request.
// This is a mandatory request to support.
func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) {
s.send(&dap.ContinueResponse{
Response: *newResponse(request.Request),
Body: dap.ContinueResponseBody{AllThreadsContinued: true}})
s.runUntilStopAndNotify(api.Continue, allowNextStateChange)
}
func fnName(loc *proc.Location) string {
if loc.Fn == nil {
return "???"
}
return loc.Fn.Name
}
func fnPackageName(loc *proc.Location) string {
if loc.Fn == nil {
// attribute unknown functions to the runtime
return "runtime"
}
return loc.Fn.PackageName()
}
// onThreadsRequest handles 'threads' request.
// This is a mandatory request to support.
// It is sent in response to configurationDone response and stopped events.
// Depending on the debug session stage, goroutines information
// might not be available. However, the DAP spec states that
// "even if a debug adapter does not support multiple threads,
// it must implement the threads request and return a single
// (dummy) thread". Therefore, this handler never returns
// an error response. If the dummy thread is returned in its place,
// the next waterfall request for its stackTrace will return the error.
func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
var err error
var gs []*proc.G
var next int
if s.debugger != nil {
gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
if err == nil {
// Parse the goroutine arguments.
filters, _, _, _, _, _, _, parseErr := api.ParseGoroutineArgs(s.args.GoroutineFilters)
if parseErr != nil {
s.logToConsole(parseErr.Error())
}
if s.args.HideSystemGoroutines {
filters = append(filters, api.ListGoroutinesFilter{
Kind: api.GoroutineUser,
Negated: false,
})
}
gs = s.debugger.FilterGoroutines(gs, filters)
}
}
var threads []dap.Thread
if err != nil {
switch err.(type) {
case proc.ErrProcessExited:
// If the program exits very quickly, the initial threads request will complete after it has exited.
// A TerminatedEvent has already been sent. Ignore the err returned in this case.
s.config.log.Debug(err)
default:
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()),
Category: "stderr",
}})
}
threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
} else if len(gs) == 0 {
threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
} else {
state, err := s.debugger.State( /*nowait*/ true)
if err != nil {
s.config.log.Debug("Unable to get debugger state: ", err)
}
if next >= 0 {
s.logToConsole(fmt.Sprintf("Too many goroutines, only loaded %d", len(gs)))
// Make sure the selected goroutine is included in the list of threads
// to return.
if state != nil && state.SelectedGoroutine != nil {
var selectedFound bool
for _, g := range gs {
if g.ID == state.SelectedGoroutine.ID {
selectedFound = true
break
}
}
if !selectedFound {
g, err := s.debugger.FindGoroutine(state.SelectedGoroutine.ID)
if err != nil {
s.config.log.Debug("Error getting selected goroutine: ", err)
} else {
// TODO(suzmue): Consider putting the selected goroutine at the top.
// To be consistent we may want to do this for all threads requests.
gs = append(gs, g)
}
}
}
}
threads = make([]dap.Thread, len(gs))
s.debugger.LockTarget()
defer s.debugger.UnlockTarget()
for i, g := range gs {
selected := ""
if state != nil && state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
selected = "* "
}
thread := ""
if g.Thread != nil && g.Thread.ThreadID() != 0 {
thread = fmt.Sprintf(" (Thread %d)", g.Thread.ThreadID())
}
// File name and line number are communicated via `stackTrace`
// so no need to include them here.
loc := g.UserCurrent()
threads[i].Name = fmt.Sprintf("%s[Go %d] %s%s", selected, g.ID, fnName(&loc), thread)
threads[i].Id = int(g.ID)
}
}
response := &dap.ThreadsResponse{
Response: *newResponse(request.Request),
Body: dap.ThreadsResponseBody{Threads: threads},
}
s.send(response)
}
// onAttachRequest handles 'attach' request.
// This is a mandatory request to support.
// Attach debug sessions support the following modes:
//
// - [DEFAULT] "local" -- attaches debugger to a local running process.
// Required args: processID
// - "remote" -- attaches client to a debugger already attached to a process.
// Required args: none (host/port are used externally to connect)
func (s *Session) onAttachRequest(request *dap.AttachRequest) {
var args AttachConfig = defaultAttachConfig // narrow copy for initializing non-zero default values
if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - %v", err))
return
}
s.config.log.Debug("parsed launch config: ", prettyPrint(args))
switch args.Mode {
case "":
args.Mode = "local"
fallthrough
case "local":
if s.debugger != nil {
s.sendShowUserErrorResponse(
request.Request, FailedToAttach,
"Failed to attach",
fmt.Sprintf("debug session already in progress at %s - use remote mode to connect to a server with an active debug session", s.address()))
return
}
if args.ProcessID == 0 {
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach",
"The 'processId' attribute is missing in debug configuration")
return
}
s.config.Debugger.AttachPid = args.ProcessID
if args.Backend == "" {
args.Backend = "default"
}
s.config.Debugger.Backend = args.Backend
s.config.log.Debugf("attaching to pid %d", args.ProcessID)
var err error
func() {
s.mu.Lock()
defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error
s.debugger, err = debugger.New(&s.config.Debugger, nil)
}()
if err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", err.Error())
return
}
// Give the user an option to terminate debuggee when client disconnects (default is to leave it)
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportTerminateDebuggee: true}}})
case "remote":
if s.debugger == nil {
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", "no debugger found")
return
}
s.config.log.Debug("debugger already started")
// Halt for configuration sequence. onConfigurationDone will restart
// execution if user requested !stopOnEntry.
s.changeStateMu.Lock()
defer s.changeStateMu.Unlock()
if _, err := s.halt(); err != nil {
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", err.Error())
return
}
// Enable StepBack controls on supported backends
if s.config.Debugger.Backend == "rr" {
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportsStepBack: true}}})
}
// Customize termination options for debugger and debuggee
if s.config.AcceptMulti {
// User can stop debugger with process or leave it running
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportTerminateDebuggee: true}}})
// TODO(polina): support SupportSuspendDebuggee when available
} else if s.config.Debugger.AttachPid > 0 {
// User can stop debugger with process or leave the process running
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportTerminateDebuggee: true}}})
} // else program was launched and the only option will be to stop both
default:
s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach",
fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", args.Mode))
return
}
s.setLaunchAttachArgs(args.LaunchAttachCommonConfig)
// 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.AttachResponse{Response: *newResponse(request.Request)})
}
// onNextRequest handles 'next' request.
// This is a mandatory request to support.
func (s *Session) onNextRequest(request *dap.NextRequest, allowNextStateChange chan struct{}) {
s.sendStepResponse(request.Arguments.ThreadId, &dap.NextResponse{Response: *newResponse(request.Request)})
s.stepUntilStopAndNotify(api.Next, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
}
// onStepInRequest handles 'stepIn' request
// This is a mandatory request to support.
func (s *Session) onStepInRequest(request *dap.StepInRequest, allowNextStateChange chan struct{}) {
s.sendStepResponse(request.Arguments.ThreadId, &dap.StepInResponse{Response: *newResponse(request.Request)})
s.stepUntilStopAndNotify(api.Step, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
}
// onStepOutRequest handles 'stepOut' request
// This is a mandatory request to support.
func (s *Session) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange chan struct{}) {
s.sendStepResponse(request.Arguments.ThreadId, &dap.StepOutResponse{Response: *newResponse(request.Request)})
s.stepUntilStopAndNotify(api.StepOut, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
}
func (s *Session) sendStepResponse(threadId int, message dap.Message) {
// All of the threads will be continued by this request, so we need to send
// a continued event so the UI can properly reflect the current state.
s.send(&dap.ContinuedEvent{
Event: *newEvent("continued"),
Body: dap.ContinuedEventBody{
ThreadId: threadId,
AllThreadsContinued: true,
},
})
s.send(message)
}
func stoppedGoroutineID(state *api.DebuggerState) (id int64) {
if state.SelectedGoroutine != nil {
id = state.SelectedGoroutine.ID
} else if state.CurrentThread != nil {
id = state.CurrentThread.GoroutineID
}
return id
}
// stoppedOnBreakpointGoroutineID gets the goroutine id of the first goroutine
// that is stopped on a real breakpoint, starting with the selected goroutine.
func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int64, *api.Breakpoint) {
// Use the first goroutine that is stopped on a breakpoint.
gs := s.stoppedGs(state)
if len(gs) == 0 {
return 0, nil
}
goid := gs[0]
if goid == 0 {
return goid, state.CurrentThread.Breakpoint
}
g, _ := s.debugger.FindGoroutine(goid)
if g == nil || g.Thread == nil {
return goid, nil
}
bp := g.Thread.Breakpoint()
if bp == nil || bp.Breakpoint == nil || bp.Breakpoint.Logical == nil {
return goid, nil
}
abp := api.ConvertLogicalBreakpoint(bp.Breakpoint.Logical)
api.ConvertPhysicalBreakpoints(abp, []int{0}, []*proc.Breakpoint{bp.Breakpoint})
return goid, abp
}
// stepUntilStopAndNotify is a wrapper around runUntilStopAndNotify that
// first switches selected goroutine. allowNextStateChange is
// a channel that will be closed to signal that an
// asynchronous command has completed setup or was interrupted
// due to an error, so the server is ready to receive new requests.
func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange chan struct{}) {
defer closeIfOpen(allowNextStateChange)
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil)
if err != nil {
s.config.log.Errorf("Error switching goroutines while stepping: %v", err)
// If we encounter an error, we will have to send a stopped event
// since we already sent the step response.
stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
stopped.Body.AllThreadsStopped = true
if state, err := s.debugger.State(false); err != nil {
s.config.log.Errorf("Error retrieving state: %e", err)
} else {
stopped.Body.ThreadId = int(stoppedGoroutineID(state))
}
stopped.Body.Reason = "error"
stopped.Body.Text = err.Error()
s.send(stopped)
return
}
if granularity == "instruction" {
switch command {
case api.ReverseNext:
command = api.ReverseStepInstruction
default:
// TODO(suzmue): consider differentiating between next, step in, and step out.
// For example, next could step over call requests.
command = api.StepInstruction
}
}
s.runUntilStopAndNotify(command, allowNextStateChange)
}
// onPauseRequest handles 'pause' request.
// This is a mandatory request to support.
func (s *Session) onPauseRequest(request *dap.PauseRequest) {
s.changeStateMu.Lock()
defer s.changeStateMu.Unlock()
s.setHaltRequested(true)
_, err := s.halt()
if err != nil {
s.sendErrorResponse(request.Request, UnableToHalt, "Unable to halt execution", err.Error())
return
}
s.send(&dap.PauseResponse{Response: *newResponse(request.Request)})
// No need to send any event here.
// If we received this request while stopped, there already was an event for the stop.
// If we received this while running, then doCommand will unblock and trigger the right
// event, using debugger.StopReason because manual stop reason always wins even if we
// simultaneously receive a manual stop request and hit a breakpoint.
}
// stackFrame represents the index of a frame within
// the context of a stack of a specific goroutine.
type stackFrame struct {
goroutineID int
frameIndex int
}
// onStackTraceRequest handles stackTrace requests.
// This is a mandatory request to support.
// As per DAP spec, this request only gets triggered as a follow-up
// to a successful threads request as part of the "request waterfall".
func (s *Session) onStackTraceRequest(request *dap.StackTraceRequest) {
if s.debugger == nil {
s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", "debugger is nil")
return
}
goroutineID := request.Arguments.ThreadId
start := request.Arguments.StartFrame
if start < 0 {
start = 0
}
levels := s.args.StackTraceDepth
if request.Arguments.Levels > 0 {
levels = request.Arguments.Levels
}
// Since the backend doesn't support paging, we load all frames up to
// the requested depth and then slice them here per
// `supportsDelayedStackTraceLoading` capability.
frames, err := s.debugger.Stacktrace(int64(goroutineID), start+levels-1, 0)
if err != nil {
s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", err.Error())
return
}
// Determine if the goroutine is a system goroutine.
isSystemGoroutine := true
if g, _ := s.debugger.FindGoroutine(int64(goroutineID)); g != nil {
isSystemGoroutine = g.System(s.debugger.Target())
}
stackFrames := []dap.StackFrame{} // initialize to empty, since nil is not an accepted response.
for i := 0; i < levels && i+start < len(frames); i++ {
frame := frames[start+i]
loc := &frame.Call
uniqueStackFrameID := s.stackFrameHandles.create(stackFrame{goroutineID, start + i})
stackFrame := dap.StackFrame{Id: uniqueStackFrameID, Line: loc.Line, Name: fnName(loc), InstructionPointerReference: fmt.Sprintf("%#x", loc.PC)}
if loc.File != "<autogenerated>" {
clientPath := s.toClientPath(loc.File)
stackFrame.Source = &dap.Source{Name: filepath.Base(clientPath), Path: clientPath}
}
stackFrame.Column = 0
packageName := fnPackageName(loc)
if !isSystemGoroutine && packageName == "runtime" {
stackFrame.PresentationHint = "subtle"
}
stackFrames = append(stackFrames, stackFrame)
}
totalFrames := len(frames)
if len(frames) >= start+levels && !frames[len(frames)-1].Bottom {
// We don't know the exact number of available stack frames, so
// add an arbitrary number so the client knows to request additional
// frames.
totalFrames += s.args.StackTraceDepth
}
response := &dap.StackTraceResponse{
Response: *newResponse(request.Request),
Body: dap.StackTraceResponseBody{StackFrames: stackFrames, TotalFrames: totalFrames},
}
s.send(response)
}
// onScopesRequest handles 'scopes' requests.
// This is a mandatory request to support.
// It is automatically sent as part of the threads > stacktrace > scopes > variables
// "waterfall" to highlight the topmost frame at stops, after an evaluate request
// for the selected scope or when a user selects different scopes in the UI.
func (s *Session) onScopesRequest(request *dap.ScopesRequest) {
sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId)
if !ok {
s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", fmt.Sprintf("unknown frame id %d", request.Arguments.FrameId))
return
}
goid := sf.(stackFrame).goroutineID
frame := sf.(stackFrame).frameIndex
// Check if the function is optimized.
fn, err := s.debugger.Function(int64(goid), frame, 0, DefaultLoadConfig)
if fn == nil || err != nil {
var details string
if err != nil {
details = err.Error()
}
s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to find enclosing function", details)
return
}
suffix := ""
if fn.Optimized() {
suffix = " (warning: optimized function)"
}
// Retrieve arguments
args, err := s.debugger.FunctionArguments(int64(goid), frame, 0, DefaultLoadConfig)
if err != nil {
s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to list args", err.Error())
return
}
// Retrieve local variables
locals, err := s.debugger.LocalVariables(int64(goid), frame, 0, DefaultLoadConfig)
if err != nil {
s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", err.Error())
return
}
locScope := &fullyQualifiedVariable{&proc.Variable{Name: fmt.Sprintf("Locals%s", suffix), Children: slicePtrVarToSliceVar(append(args, locals...))}, "", true, 0}
scopeLocals := dap.Scope{Name: locScope.Name, VariablesReference: s.variableHandles.create(locScope)}
scopes := []dap.Scope{scopeLocals}
if s.args.ShowGlobalVariables {
// Limit what global variables we will return to the current package only.
// TODO(polina): This is how vscode-go currently does it to make
// the amount of the returned data manageable. In fact, this is
// considered so expensive even with the package filter, that
// the default for showGlobalVariables was recently flipped to
// not showing. If we delay loading of the globals until the corresponding
// scope is expanded, generating an explicit variable request,
// should we consider making all globals accessible with a scope per package?
// Or users can just rely on watch variables.
currPkg, err := s.debugger.CurrentPackage()
if err != nil {
s.sendErrorResponse(request.Request, UnableToListGlobals, "Unable to list globals", err.Error())
return
}
currPkgFilter := fmt.Sprintf("^%s\\.", currPkg)
globals, err := s.debugger.PackageVariables(currPkgFilter, DefaultLoadConfig)
if err != nil {
s.sendErrorResponse(request.Request, UnableToListGlobals, "Unable to list globals", err.Error())
return
}
// Remove package prefix from the fully-qualified variable names.
// We will include the package info once in the name of the scope instead.
for i, g := range globals {
globals[i].Name = strings.TrimPrefix(g.Name, currPkg+".")
}
globScope := &fullyQualifiedVariable{&proc.Variable{
Name: fmt.Sprintf("Globals (package %s)", currPkg),
Children: slicePtrVarToSliceVar(globals),
}, currPkg, true, 0}
scopeGlobals := dap.Scope{Name: globScope.Name, VariablesReference: s.variableHandles.create(globScope)}
scopes = append(scopes, scopeGlobals)
}
if s.args.ShowRegisters {
// Retrieve registers
regs, err := s.debugger.ScopeRegisters(int64(goid), frame, 0, false)
if err != nil {
s.sendErrorResponse(request.Request, UnableToListRegisters, "Unable to list registers", err.Error())
return
}
outRegs := api.ConvertRegisters(regs, s.debugger.DwarfRegisterToString, false)
regsVar := make([]proc.Variable, len(outRegs))
for i, r := range outRegs {
regsVar[i] = proc.Variable{
Name: r.Name,
Value: constant.MakeString(r.Value),
Kind: reflect.Kind(proc.VariableConstant),
}
}
regsScope := &fullyQualifiedVariable{&proc.Variable{Name: "Registers", Children: regsVar}, "", true, 0}
scopeRegisters := dap.Scope{Name: regsScope.Name, VariablesReference: s.variableHandles.create(regsScope)}
scopes = append(scopes, scopeRegisters)
}
response := &dap.ScopesResponse{
Response: *newResponse(request.Request),
Body: dap.ScopesResponseBody{Scopes: scopes},
}
s.send(response)
}
func slicePtrVarToSliceVar(vars []*proc.Variable) []proc.Variable {
r := make([]proc.Variable, len(vars))
for i := range vars {
r[i] = *vars[i]
}
return r
}
// onVariablesRequest handles 'variables' requests.
// This is a mandatory request to support.
func (s *Session) onVariablesRequest(request *dap.VariablesRequest) {
ref := request.Arguments.VariablesReference
v, ok := s.variableHandles.get(ref)
if !ok {
s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", fmt.Sprintf("unknown reference %d", ref))
return
}
// If there is a filter applied, we will need to create a new variable that includes
// the values actually needed to load. This cannot be done when loading the parent
// node, since it is unknown at that point which children will need to be loaded.
if request.Arguments.Filter == "indexed" {
var err error
v, err = s.maybeLoadResliced(v, request.Arguments.Start, request.Arguments.Count)
if err != nil {
s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
return
}
}
children := []dap.Variable{} // must return empty array, not null, if no children
if request.Arguments.Filter == "named" || request.Arguments.Filter == "" {
named, err := s.metadataToDAPVariables(v)
if err != nil {
s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
return
}
children = append(children, named...)
}
if request.Arguments.Filter == "indexed" || request.Arguments.Filter == "" {
indexed := s.childrenToDAPVariables(v)
children = append(children, indexed...)
}
response := &dap.VariablesResponse{
Response: *newResponse(request.Request),
Body: dap.VariablesResponseBody{Variables: children},
}
s.send(response)
}
func (s *Session) maybeLoadResliced(v *fullyQualifiedVariable, start, count int) (*fullyQualifiedVariable, error) {
if start == 0 {
want := count
if v.Kind == reflect.Map {
// For maps, we need to have 2*count children since each key-value pair is two variables.
want *= 2
}
if want == len(v.Children) {
// If we have already loaded the correct children,
// just return the variable.
return v, nil
}
}
indexedLoadConfig := DefaultLoadConfig
indexedLoadConfig.MaxArrayValues = count
newV, err := s.debugger.LoadResliced(v.Variable, start, indexedLoadConfig)
if err != nil {
return nil, err
}
return &fullyQualifiedVariable{newV, v.fullyQualifiedNameOrExpr, false, start}, nil
}
// getIndexedVariableCount returns the number of indexed variables
// for a DAP variable. For maps this may be less than the actual
// number of children returned, since a key-value pair may be split
// into two separate children.
func getIndexedVariableCount(v *proc.Variable) int {
indexedVars := 0
switch v.Kind {
case reflect.Array, reflect.Slice, reflect.Map:
indexedVars = int(v.Len)
}
return indexedVars
}
// childrenToDAPVariables returns the DAP presentation of the referenced variable's children.
func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) []dap.Variable {
// TODO(polina): consider convertVariableToString instead of convertVariable
// and avoid unnecessary creation of variable handles when this is called to
// compute evaluate names when this is called from onSetVariableRequest.
children := []dap.Variable{} // must return empty array, not null, if no children
switch v.Kind {
case reflect.Map:
for i := 0; i < len(v.Children); i += 2 {
// A map will have twice as many children as there are key-value elements.
kvIndex := i / 2
// Process children in pairs: even indices are map keys, odd indices are values.
keyv, valv := &v.Children[i], &v.Children[i+1]
keyexpr := fmt.Sprintf("(*(*%q)(%#x))", keyv.TypeString(), keyv.Addr)
valexpr := fmt.Sprintf("%s[%s]", v.fullyQualifiedNameOrExpr, keyexpr)
switch keyv.Kind {
// For value expression, use the key value, not the corresponding expression if the key is a scalar.
case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
valexpr = fmt.Sprintf("%s[%s]", v.fullyQualifiedNameOrExpr, api.VariableValueAsString(keyv))
case reflect.String:
if key := constant.StringVal(keyv.Value); keyv.Len == int64(len(key)) { // fully loaded
valexpr = fmt.Sprintf("%s[%q]", v.fullyQualifiedNameOrExpr, key)
}
}
key, keyref := s.convertVariable(keyv, keyexpr)
val, valref := s.convertVariable(valv, valexpr)
keyType := s.getTypeIfSupported(keyv)
valType := s.getTypeIfSupported(valv)
// If key or value or both are scalars, we can use
// a single variable to represent key:value format.
// Otherwise, we must return separate variables for both.
if keyref > 0 && valref > 0 { // Both are not scalars
keyvar := dap.Variable{
Name: fmt.Sprintf("[key %d]", v.startIndex+kvIndex),
EvaluateName: keyexpr,
Type: keyType,
Value: key,
VariablesReference: keyref,
IndexedVariables: getIndexedVariableCount(keyv),
NamedVariables: getNamedVariableCount(keyv),
}
valvar := dap.Variable{
Name: fmt.Sprintf("[val %d]", v.startIndex+kvIndex),
EvaluateName: valexpr,
Type: valType,
Value: val,
VariablesReference: valref,
IndexedVariables: getIndexedVariableCount(valv),
NamedVariables: getNamedVariableCount(valv),
}
children = append(children, keyvar, valvar)
} else { // At least one is a scalar
keyValType := valType
if len(keyType) > 0 && len(valType) > 0 {
keyValType = fmt.Sprintf("%s: %s", keyType, valType)
}
kvvar := dap.Variable{
Name: key,
EvaluateName: valexpr,
Type: keyValType,
Value: val,
}
if keyref != 0 { // key is a type to be expanded
if len(key) > maxMapKeyValueLen {
// Truncate and make unique
kvvar.Name = fmt.Sprintf("%s... @ %#x", key[0:maxMapKeyValueLen], keyv.Addr)
}
kvvar.VariablesReference = keyref
kvvar.IndexedVariables = getIndexedVariableCount(keyv)
kvvar.NamedVariables = getNamedVariableCount(keyv)
} else if valref != 0 { // val is a type to be expanded
kvvar.VariablesReference = valref
kvvar.IndexedVariables = getIndexedVariableCount(valv)
kvvar.NamedVariables = getNamedVariableCount(valv)
}
children = append(children, kvvar)
}
}
case reflect.Slice, reflect.Array:
children = make([]dap.Variable, len(v.Children))
for i := range v.Children {
idx := v.startIndex + i
cfqname := fmt.Sprintf("%s[%d]", v.fullyQualifiedNameOrExpr, idx)
cvalue, cvarref := s.convertVariable(&v.Children[i], cfqname)
children[i] = dap.Variable{
Name: fmt.Sprintf("[%d]", idx),
EvaluateName: cfqname,
Type: s.getTypeIfSupported(&v.Children[i]),
Value: cvalue,
VariablesReference: cvarref,
IndexedVariables: getIndexedVariableCount(&v.Children[i]),
NamedVariables: getNamedVariableCount(&v.Children[i]),
}
}
default:
children = make([]dap.Variable, len(v.Children))
for i := range v.Children {
c := &v.Children[i]
cfqname := fmt.Sprintf("%s.%s", v.fullyQualifiedNameOrExpr, c.Name)
if strings.HasPrefix(c.Name, "~") || strings.HasPrefix(c.Name, ".") {
cfqname = ""
} else if v.isScope && v.fullyQualifiedNameOrExpr == "" {
cfqname = c.Name
} else if v.fullyQualifiedNameOrExpr == "" {
cfqname = ""
} else if v.Kind == reflect.Interface {
cfqname = fmt.Sprintf("%s.(%s)", v.fullyQualifiedNameOrExpr, c.Name) // c is data
} else if v.Kind == reflect.Ptr {
cfqname = fmt.Sprintf("(*%v)", v.fullyQualifiedNameOrExpr) // c is the nameless pointer value
} else if v.Kind == reflect.Complex64 || v.Kind == reflect.Complex128 {
cfqname = "" // complex children are not struct fields and can't be accessed directly
}
cvalue, cvarref := s.convertVariable(c, cfqname)
// Annotate any shadowed variables to "(name)" in order
// to distinguish from non-shadowed variables.
// TODO(suzmue): should we support a special evaluateName syntax that
// can access shadowed variables?
name := c.Name
if c.Flags&proc.VariableShadowed == proc.VariableShadowed {
name = fmt.Sprintf("(%s)", name)
}
if v.isScope && v.Name == "Registers" {
// Align all of the register names.
name = fmt.Sprintf("%6s", strings.ToLower(c.Name))
// Set the correct evaluate name for the register.
cfqname = fmt.Sprintf("_%s", strings.ToUpper(c.Name))
// Unquote the value
if ucvalue, err := strconv.Unquote(cvalue); err == nil {
cvalue = ucvalue
}
}
children[i] = dap.Variable{
Name: name,
EvaluateName: cfqname,
Type: s.getTypeIfSupported(c),
Value: cvalue,
VariablesReference: cvarref,
IndexedVariables: getIndexedVariableCount(c),
NamedVariables: getNamedVariableCount(c),
}
}
}
return children
}
func getNamedVariableCount(v *proc.Variable) int {
namedVars := 0
if v.Kind == reflect.Map && v.Len > 0 {
// len
namedVars += 1
}
if isListOfBytesOrRunes(v) {
// string value of array/slice of bytes and runes.
namedVars += 1
}
return namedVars
}
// metadataToDAPVariables returns the DAP presentation of the referenced variable's metadata.
// These are included as named variables
func (s *Session) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
children := []dap.Variable{} // must return empty array, not null, if no children
if v.Kind == reflect.Map && v.Len > 0 {
children = append(children, dap.Variable{
Name: "len()",
Value: fmt.Sprintf("%d", v.Len),
Type: "int",
EvaluateName: fmt.Sprintf("len(%s)", v.fullyQualifiedNameOrExpr),
})
}
if isListOfBytesOrRunes(v.Variable) {
// Return the string value of []byte or []rune.
typeName := api.PrettyTypeName(v.DwarfType)
loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr)
s.config.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr)
// We know that this is an array/slice of Uint8 or Int32, so we will load up to MaxStringLen.
config := DefaultLoadConfig
config.MaxArrayValues = config.MaxStringLen
vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config)
if err == nil {
val := s.convertVariableToString(vLoaded)
// TODO(suzmue): Add evaluate name. Using string(name) will not get the same result because the
// MaxArrayValues is not auto adjusted in evaluate requests like MaxStringLen is adjusted.
children = append(children, dap.Variable{
Name: "string()",
Value: val,
Type: "string",
})
} else {
s.config.log.Debugf("failed to load %q: %v", v.fullyQualifiedNameOrExpr, err)
}
}
return children, nil
}
func isListOfBytesOrRunes(v *proc.Variable) bool {
if len(v.Children) > 0 && (v.Kind == reflect.Array || v.Kind == reflect.Slice) {
childKind := v.Children[0].RealType.Common().ReflectKind
return childKind == reflect.Uint8 || childKind == reflect.Int32
}
return false
}
func (s *Session) getTypeIfSupported(v *proc.Variable) string {
if !s.clientCapabilities.supportsVariableType {
return ""
}
return v.TypeString()
}
// convertVariable converts proc.Variable to dap.Variable value and reference
// while keeping track of the full qualified name or load expression.
// Variable reference is used to keep track of the children associated with each
// variable. It is shared with the host via scopes or evaluate response and is an index
// into the s.variableHandles map, used to look up variables and their children on
// subsequent variables requests. A positive reference signals the host that another
// variables request can be issued to get the elements of the compound variable. As a
// custom, a zero reference, reminiscent of a zero pointer, is used to indicate that
// a scalar variable cannot be "dereferenced" to get its elements (as there are none).
func (s *Session) convertVariable(v *proc.Variable, qualifiedNameOrExpr string) (value string, variablesReference int) {
return s.convertVariableWithOpts(v, qualifiedNameOrExpr, 0)
}
func (s *Session) convertVariableToString(v *proc.Variable) string {
val, _ := s.convertVariableWithOpts(v, "", skipRef)
return val
}
const (
// Limit the length of a string representation of a compound or reference type variable.
maxVarValueLen = 1 << 8 // 256
// Limit the length of an inlined map key.
maxMapKeyValueLen = 64
)
// Flags for convertVariableWithOpts option.
type convertVariableFlags uint8
const (
skipRef convertVariableFlags = 1 << iota
showFullValue
)
// convertVariableWithOpts allows to skip reference generation in case all we need is
// a string representation of the variable. When the variable is a compound or reference
// type variable and its full string representation can be larger than defaultMaxValueLen,
// this returns a truncated value unless showFull option flag is set.
func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, opts convertVariableFlags) (value string, variablesReference int) {
canHaveRef := false
maybeCreateVariableHandle := func(v *proc.Variable) int {
canHaveRef = true
if opts&skipRef != 0 {
return 0
}
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/, 0})
}
value = api.ConvertVar(v).SinglelineString()
if v.Unreadable != nil {
return value, 0
}
// Some of the types might be fully or partially not loaded based on LoadConfig.
// Those that are fully missing (e.g. due to hitting MaxVariableRecurse), can be reloaded in place.
var reloadVariable = func(v *proc.Variable, qualifiedNameOrExpr string) (value string) {
// We might be loading variables from the frame that's not topmost, so use
// frame-independent address-based expression, not fully-qualified name as per
// https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables.
// TODO(polina): Get *proc.Variable object from debugger instead. Export a function to set v.loaded to false
// and call v.loadValue gain with a different load config. It's more efficient, and it's guaranteed to keep
// working with generics.
value = api.ConvertVar(v).SinglelineString()
typeName := api.PrettyTypeName(v.DwarfType)
loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr)
s.config.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr)
// Make sure we can load the pointers directly, not by updating just the child
// This is not really necessary now because users have no way of setting FollowPointers to false.
config := DefaultLoadConfig
config.FollowPointers = true
vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config)
if err != nil {
value += fmt.Sprintf(" - FAILED TO LOAD: %s", err)
} else {
v.Children = vLoaded.Children
v.Value = vLoaded.Value
value = api.ConvertVar(v).SinglelineString()
}
return value
}
switch v.Kind {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
n, _ := strconv.ParseUint(api.ConvertVar(v).Value, 10, 64)
value = fmt.Sprintf("%s = %#x", value, n)
case reflect.UnsafePointer:
// Skip child reference
case reflect.Ptr:
if v.DwarfType != nil && len(v.Children) > 0 && v.Children[0].Addr != 0 && v.Children[0].Kind != reflect.Invalid {
if v.Children[0].OnlyAddr { // Not loaded
if v.Addr == 0 {
// This is equivalent to the following with the cli:
// (dlv) p &a7
// (**main.FooBar)(0xc0000a3918)
//
// TODO(polina): what is more appropriate?
// Option 1: leave it unloaded because it is a special case
// Option 2: load it, but then we have to load the child, not the parent, unlike all others
// TODO(polina): see if reloadVariable can be reused here
cTypeName := api.PrettyTypeName(v.Children[0].DwarfType)
cLoadExpr := fmt.Sprintf("*(*%q)(%#x)", cTypeName, v.Children[0].Addr)
s.config.log.Debugf("loading *(%s) (type %s) with %s", qualifiedNameOrExpr, cTypeName, cLoadExpr)
cLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, cLoadExpr, DefaultLoadConfig)
if err != nil {
value += fmt.Sprintf(" - FAILED TO LOAD: %s", err)
} else {
cLoaded.Name = v.Children[0].Name // otherwise, this will be the pointer expression
v.Children = []proc.Variable{*cLoaded}
value = api.ConvertVar(v).SinglelineString()
}
} else {
value = reloadVariable(v, qualifiedNameOrExpr)
}
}
if !v.Children[0].OnlyAddr {
variablesReference = maybeCreateVariableHandle(v)
}
}
case reflect.Slice, reflect.Array:
if v.Len > int64(len(v.Children)) { // Not fully loaded
if v.Base != 0 && len(v.Children) == 0 { // Fully missing
value = reloadVariable(v, qualifiedNameOrExpr)
} else if !s.clientCapabilities.supportsVariablePaging {
value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children), v.Len) + value
}
}
if v.Base != 0 && len(v.Children) > 0 {
variablesReference = maybeCreateVariableHandle(v)
}
case reflect.Map:
if v.Len > int64(len(v.Children)/2) { // Not fully loaded
if len(v.Children) == 0 { // Fully missing
value = reloadVariable(v, qualifiedNameOrExpr)
} else if !s.clientCapabilities.supportsVariablePaging {
value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children)/2, v.Len) + value
}
}
if v.Base != 0 && len(v.Children) > 0 {
variablesReference = maybeCreateVariableHandle(v)
}
case reflect.String:
// TODO(polina): implement auto-loading here.
case reflect.Interface:
if v.Addr != 0 && len(v.Children) > 0 && v.Children[0].Kind != reflect.Invalid && v.Children[0].Addr != 0 {
if v.Children[0].OnlyAddr { // Not loaded
value = reloadVariable(v, qualifiedNameOrExpr)
}
if !v.Children[0].OnlyAddr {
variablesReference = maybeCreateVariableHandle(v)
}
}
case reflect.Struct:
if v.Len > int64(len(v.Children)) { // Not fully loaded
if len(v.Children) == 0 { // Fully missing
value = reloadVariable(v, qualifiedNameOrExpr)
} else { // Partially missing (TODO)
value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children), v.Len) + value
}
}
if len(v.Children) > 0 {
variablesReference = maybeCreateVariableHandle(v)
}
case reflect.Complex64, reflect.Complex128:
v.Children = make([]proc.Variable, 2)
v.Children[0].Name = "real"
v.Children[0].Value = constant.Real(v.Value)
v.Children[1].Name = "imaginary"
v.Children[1].Value = constant.Imag(v.Value)
if v.Kind == reflect.Complex64 {
v.Children[0].Kind = reflect.Float32
v.Children[1].Kind = reflect.Float32
} else {
v.Children[0].Kind = reflect.Float64
v.Children[1].Kind = reflect.Float64
}
fallthrough
default: // Complex, Scalar, Chan, Func
if len(v.Children) > 0 {
variablesReference = maybeCreateVariableHandle(v)
}
}
// By default, only values of variables that have children can be truncated.
// If showFullValue is set, then all value strings are not truncated.
canTruncateValue := showFullValue&opts == 0
if len(value) > maxVarValueLen && canTruncateValue && canHaveRef {
value = value[:maxVarValueLen] + "..."
}
return value, variablesReference
}
// onEvaluateRequest handles 'evaluate' requests.
// This is a mandatory request to support.
// Support the following expressions:
//
// - {expression} - evaluates the expression and returns the result as a variable
// - call {function} - injects a function call and returns the result as a variable
// - config {expression} - updates configuration parameters
//
// TODO(polina): users have complained about having to click to expand multi-level
// variables, so consider also adding the following:
//
// - print {expression} - return the result as a string like from dlv cli
func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) {
showErrorToUser := request.Arguments.Context != "watch" && request.Arguments.Context != "repl" && request.Arguments.Context != "hover"
if s.debugger == nil {
s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "debugger is nil", showErrorToUser)
return
}
// Default to the topmost stack frame of the current goroutine in case
// no frame is specified (e.g. when stopped on entry or no call stack frame is expanded)
goid, frame := -1, 0
if sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId); ok {
goid = sf.(stackFrame).goroutineID
frame = sf.(stackFrame).frameIndex
}
response := &dap.EvaluateResponse{Response: *newResponse(request.Request)}
expr := request.Arguments.Expression
if isConfig, err := regexp.MatchString(`^\s*dlv\s+\S+`, expr); err == nil && isConfig { // dlv {command}
expr := strings.Replace(expr, "dlv ", "", 1)
result, err := s.delveCmd(goid, frame, expr)
if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToRunDlvCommand, "Unable to run dlv command", err.Error(), showErrorToUser)
return
}
response.Body = dap.EvaluateResponseBody{
Result: result,
}
} else if isCall, err := regexp.MatchString(`^\s*call\s+\S+`, expr); err == nil && isCall { // call {expression}
expr := strings.Replace(expr, "call ", "", 1)
_, retVars, err := s.doCall(goid, frame, expr)
if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
return
}
// The call completed and we can reply with its return values (if any)
if len(retVars) > 0 {
// Package one or more return values in a single scope-like nameless variable
// that preserves their names.
retVarsAsVar := &proc.Variable{Children: slicePtrVarToSliceVar(retVars)}
// As a shortcut also express the return values as a single string.
retVarsAsStr := ""
for _, v := range retVars {
retVarsAsStr += s.convertVariableToString(v) + ", "
}
response.Body = dap.EvaluateResponseBody{
Result: strings.TrimRight(retVarsAsStr, ", "),
VariablesReference: s.variableHandles.create(&fullyQualifiedVariable{retVarsAsVar, "", false /*not a scope*/, 0}),
}
}
} else { // {expression}
exprVar, err := s.debugger.EvalVariableInScope(int64(goid), frame, 0, expr, DefaultLoadConfig)
if err != nil {
s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", err.Error(), showErrorToUser)
return
}
ctxt := request.Arguments.Context
switch ctxt {
case "repl", "variables", "hover", "clipboard":
if exprVar.Kind == reflect.String {
if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) {
// Reload the string value with a bigger limit.
loadCfg := DefaultLoadConfig
loadCfg.MaxStringLen = maxSingleStringLen
if v, err := s.debugger.EvalVariableInScope(int64(goid), frame, 0, request.Arguments.Expression, loadCfg); err != nil {
s.config.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
} else {
exprVar = v
}
}
}
}
var opts convertVariableFlags
// Send the full value when the context is "clipboard" or "variables" since
// these contexts are used to copy the value.
if ctxt == "clipboard" || ctxt == "variables" {
opts |= showFullValue
}
exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts)
response.Body = dap.EvaluateResponseBody{Result: exprVal, Type: s.getTypeIfSupported(exprVar), VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar), NamedVariables: getNamedVariableCount(exprVar)}
}
s.send(response)
}
func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*proc.Variable, error) {
// This call might be evaluated in the context of the frame that is not topmost
// if the editor is set to view the variables for one of the parent frames.
// If the call expression refers to any of these variables, unlike regular
// expressions, it will evaluate them in the context of the topmost frame,
// and the user will get an unexpected result or an unexpected symbol error.
// We prevent this but disallowing any frames other than topmost.
if frame > 0 {
return nil, nil, fmt.Errorf("call is only supported with topmost stack frame")
}
stateBeforeCall, err := s.debugger.State( /*nowait*/ true)
if err != nil {
return nil, nil, err
}
// The return values of injected function calls are volatile.
// Load as much useful data as possible.
// TODO: investigate whether we need to increase other limits. For example,
// the return value is a pointer to a temporary object, which can become
// invalid by other injected function calls. Do we care about such use cases?
loadCfg := DefaultLoadConfig
loadCfg.MaxStringLen = maxStringLenInCallRetVars
// TODO(polina): since call will resume execution of all goroutines,
// we should do this asynchronously and send a continued event to the
// editor, followed by a stop event when the call completes.
state, err := s.debugger.Command(&api.DebuggerCommand{
Name: api.Call,
ReturnInfoLoadConfig: api.LoadConfigFromProc(&loadCfg),
Expr: expr,
UnsafeCall: false,
GoroutineID: int64(goid),
}, nil)
if processExited(state, err) {
s.preTerminatedWG.Wait()
e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
s.send(e)
return nil, nil, errors.New("terminated")
}
if err != nil {
return nil, nil, err
}
// After the call is done, the goroutine where we injected the call should
// return to the original stopped line with return values. However,
// it is not guaranteed to be selected due to the possibility of the
// of simultaneous breakpoints. Therefore, we check all threads.
var retVars []*proc.Variable
found := false
for _, t := range state.Threads {
if t.GoroutineID == stateBeforeCall.SelectedGoroutine.ID &&
t.Line == stateBeforeCall.SelectedGoroutine.CurrentLoc.Line && t.CallReturn {
found = true
// The call completed. Get the return values.
retVars, err = s.debugger.FindThreadReturnValues(t.ID, loadCfg)
if err != nil {
return nil, nil, err
}
break
}
}
// Normal function calls expect return values, but call commands
// used for variable assignments do not return a value when they succeed.
// In go '=' is not an operator. Check if go/parser complains.
// If the above Call command passed but the expression is not a valid
// go expression, we just handled a variable assignment request.
isAssignment := false
if _, err := parser.ParseExpr(expr); err != nil {
isAssignment = true
}
// note: as described in https://github.com/golang/go/issues/25578, function call injection
// causes to resume the entire Go process. Due to this limitation, there is no guarantee
// that the process is in the same state even after the injected call returns normally
// without any surprises such as breakpoints or panic. To handle this correctly we need
// to reset all the handles (both variables and stack frames).
//
// We considered sending a stopped event after each call unconditionally, but a stopped
// event can be expensive and can interact badly with the client-side optimization
// to refresh information. For example, VS Code reissues scopes/evaluate (for watch) after
// completing a setVariable or evaluate request for repl context. Thus, for now, we
// do not trigger a stopped event and hope editors to refetch the updated state as soon
// as the user resumes debugging.
if !found || !isAssignment && retVars == nil {
// The call got interrupted by a stop (e.g. breakpoint in injected
// function call or in another goroutine).
s.resetHandlesForStoppedEvent()
s.sendStoppedEvent(state)
// TODO(polina): once this is asynchronous, we could wait to reply until the user
// continues, call ends, original stop point is hit and return values are available
// instead of returning an error 'call stopped' here.
return nil, nil, errors.New("call stopped")
}
return state, retVars, nil
}
func (s *Session) sendStoppedEvent(state *api.DebuggerState) {
stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
stopped.Body.AllThreadsStopped = true
stopped.Body.ThreadId = int(stoppedGoroutineID(state))
stopped.Body.Reason = s.debugger.StopReason().String()
s.send(stopped)
}
// onTerminateRequest sends a not-yet-implemented error response.
// Capability 'supportsTerminateRequest' is not set in 'initialize' response.
func (s *Session) onTerminateRequest(request *dap.TerminateRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onRestartRequest sends a not-yet-implemented error response
// Capability 'supportsRestartRequest' is not set in 'initialize' response.
func (s *Session) onRestartRequest(request *dap.RestartRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onStepBackRequest handles 'stepBack' request.
// This is an optional request enabled by capability supportsStepBackRequest.
func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange chan struct{}) {
s.sendStepResponse(request.Arguments.ThreadId, &dap.StepBackResponse{Response: *newResponse(request.Request)})
s.stepUntilStopAndNotify(api.ReverseNext, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
}
// onReverseContinueRequest performs a rewind command call up to the previous
// breakpoint or the start of the process
// This is an optional request enabled by capability supportsStepBackRequest.
func (s *Session) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange chan struct{}) {
s.send(&dap.ReverseContinueResponse{
Response: *newResponse(request.Request),
})
s.runUntilStopAndNotify(api.Rewind, allowNextStateChange)
}
// computeEvaluateName finds the named child, and computes its evaluate name.
func (s *Session) computeEvaluateName(v *fullyQualifiedVariable, cname string) (string, error) {
children := s.childrenToDAPVariables(v)
for _, c := range children {
if c.Name == cname {
if c.EvaluateName != "" {
return c.EvaluateName, nil
}
return "", errors.New("cannot set the variable without evaluate name")
}
}
return "", errors.New("failed to find the named variable")
}
// onSetVariableRequest handles 'setVariable' requests.
func (s *Session) onSetVariableRequest(request *dap.SetVariableRequest) {
arg := request.Arguments
v, ok := s.variableHandles.get(arg.VariablesReference)
if !ok {
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to lookup variable", fmt.Sprintf("unknown reference %d", arg.VariablesReference))
return
}
// We need to translate the arg.Name to its evaluateName if the name
// refers to a field or element of a variable.
// https://github.com/microsoft/vscode/issues/120774
evaluateName, err := s.computeEvaluateName(v, arg.Name)
if err != nil {
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
return
}
// By running EvalVariableInScope, we get the type info of the variable
// that can be accessed with the evaluateName, and ensure the variable we are
// trying to update is valid and accessible from the top most frame & the
// current goroutine.
goid, frame := -1, 0
evaluated, err := s.debugger.EvalVariableInScope(int64(goid), frame, 0, evaluateName, DefaultLoadConfig)
if err != nil {
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to lookup variable", err.Error())
return
}
useFnCall := false
switch evaluated.Kind {
case reflect.String:
useFnCall = true
default:
// TODO(hyangah): it's possible to set a non-string variable using (`call i = fn()`)
// and we don't support it through the Set Variable request yet.
// If we want to support it for non-string types, we need to parse arg.Value.
}
if useFnCall {
// TODO(hyangah): function call injection currently allows to assign return values of
// a function call to variables. So, curious users would find set variable
// on string would accept expression like `fn()`.
if state, retVals, err := s.doCall(goid, frame, fmt.Sprintf("%v=%v", evaluateName, arg.Value)); err != nil {
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
return
} else if retVals != nil {
// The assignment expression isn't supposed to return values, but we got them.
// That indicates something went wrong (e.g. panic).
// TODO: isn't it simpler to do this in s.doCall?
s.resetHandlesForStoppedEvent()
s.sendStoppedEvent(state)
var r []string
for _, v := range retVals {
r = append(r, s.convertVariableToString(v))
}
msg := "interrupted"
if len(r) > 0 {
msg = "interrupted:" + strings.Join(r, ", ")
}
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", msg)
return
}
} else {
if err := s.debugger.SetVariableInScope(int64(goid), frame, 0, evaluateName, arg.Value); err != nil {
s.sendErrorResponse(request.Request, UnableToSetVariable, "Unable to set variable", err.Error())
return
}
}
// * Note on inconsistent state after set variable:
//
// The variable handles may be in inconsistent state - for example,
// let's assume there are two aliased variables pointing to the same
// memory and both are already loaded and cached in the variable handle.
// VSCode tries to locally update the UI when the set variable
// request succeeds, and may issue additional scopes or evaluate requests
// to update the variable/watch sections if necessary.
//
// More complicated situation is when the set variable involves call
// injection - after the injected call is completed, the debuggee can
// be in a completely different state (see the note in doCall) due to
// how the call injection is implemented. Ideally, we need to also refresh
// the stack frames but that is complicated. For now we don't try to actively
// invalidate this state hoping that the editors will refetch the state
// as soon as the user resumes debugging.
response := &dap.SetVariableResponse{Response: *newResponse(request.Request)}
response.Body.Value = arg.Value
// TODO(hyangah): instead of arg.Value, reload the variable and return
// the presentation of the new value.
s.send(response)
}
// onSetExpression sends a not-yet-implemented error response.
// Capability 'supportsSetExpression' is not set 'initialize' response.
func (s *Session) onSetExpressionRequest(request *dap.SetExpressionRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onLoadedSourcesRequest sends a not-yet-implemented error response.
// Capability 'supportsLoadedSourcesRequest' is not set 'initialize' response.
func (s *Session) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onReadMemoryRequest sends a not-yet-implemented error response.
// Capability 'supportsReadMemoryRequest' is not set 'initialize' response.
func (s *Session) onReadMemoryRequest(request *dap.ReadMemoryRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
var invalidInstruction = dap.DisassembledInstruction{
Instruction: "invalid instruction",
}
// onDisassembleRequest handles 'disassemble' requests.
// Capability 'supportsDisassembleRequest' is set in 'initialize' response.
func (s *Session) onDisassembleRequest(request *dap.DisassembleRequest) {
// TODO(suzmue): microsoft/vscode#129655 is discussing the difference between
// memory reference and instructionPointerReference, which are currently
// being used interchangeably by vscode.
addr, err := strconv.ParseUint(request.Arguments.MemoryReference, 0, 64)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
// If the requested memory address is an invalid location, return all invalid instructions.
// TODO(suzmue): consider adding fake addresses that would allow us to receive out of bounds
// requests that include valid instructions designated by InstructionOffset or InstructionCount.
if addr == 0 || addr == uint64(math.MaxUint64) {
instructions := make([]dap.DisassembledInstruction, request.Arguments.InstructionCount)
for i := range instructions {
instructions[i] = invalidInstruction
instructions[i].Address = request.Arguments.MemoryReference
}
response := &dap.DisassembleResponse{
Response: *newResponse(request.Request),
Body: dap.DisassembleResponseBody{
Instructions: instructions,
},
}
s.send(response)
return
}
start := uint64(addr)
maxInstructionLength := s.debugger.Target().BinInfo().Arch.MaxInstructionLength()
byteOffset := request.Arguments.InstructionOffset * maxInstructionLength
// Adjust the offset to include instructions before the requested address.
if byteOffset < 0 {
start = uint64(int(addr) + byteOffset)
}
// Adjust the number of instructions to include enough instructions after
// the requested address.
count := request.Arguments.InstructionCount
if byteOffset > 0 {
count += byteOffset
}
end := uint64(int(addr) + count*maxInstructionLength)
// Make sure the PCs are lined up with instructions.
start, end = alignPCs(s.debugger.Target().BinInfo(), start, end)
// Disassemble the instructions
procInstructions, err := s.debugger.Disassemble(-1, start, end)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
// Find the section of instructions that were requested.
procInstructions, offset, err := findInstructions(procInstructions, addr, request.Arguments.InstructionOffset, request.Arguments.InstructionCount)
if err != nil {
s.sendErrorResponse(request.Request, UnableToDisassemble, "Unable to disassemble", err.Error())
return
}
// Turn the given range of instructions into dap instructions.
instructions := make([]dap.DisassembledInstruction, request.Arguments.InstructionCount)
lastFile, lastLine := "", -1
for i := range instructions {
// i is not in a valid range, use an address that is just before or after
// the range. This ensures that it can still be parsed as an int.
if i < offset {
// i is not in a valid range.
instructions[i] = invalidInstruction
instructions[i].Address = "0x0"
continue
}
if (i - offset) >= len(procInstructions) {
// i is not in a valid range.
instructions[i] = invalidInstruction
instructions[i].Address = fmt.Sprintf("%#x", uint64(math.MaxUint64))
continue
}
instruction := api.ConvertAsmInstruction(procInstructions[i-offset], s.debugger.AsmInstructionText(&procInstructions[i-offset], proc.GoFlavour))
instructions[i] = dap.DisassembledInstruction{
Address: fmt.Sprintf("%#x", instruction.Loc.PC),
InstructionBytes: fmt.Sprintf("%x", instruction.Bytes),
Instruction: instruction.Text,
}
// Only set the location on the first instruction for a given line.
if instruction.Loc.File != lastFile || instruction.Loc.Line != lastLine {
instructions[i].Location = &dap.Source{Path: instruction.Loc.File}
instructions[i].Line = instruction.Loc.Line
lastFile, lastLine = instruction.Loc.File, instruction.Loc.Line
}
}
response := &dap.DisassembleResponse{
Response: *newResponse(request.Request),
Body: dap.DisassembleResponseBody{
Instructions: instructions,
},
}
s.send(response)
}
func findInstructions(procInstructions []proc.AsmInstruction, addr uint64, instructionOffset, count int) ([]proc.AsmInstruction, int, error) {
ref := sort.Search(len(procInstructions), func(i int) bool {
return procInstructions[i].Loc.PC >= addr
})
if ref == len(procInstructions) || procInstructions[ref].Loc.PC != uint64(addr) {
return nil, -1, fmt.Errorf("could not find memory reference")
}
// offset is the number of instructions that should appear before the first instruction
// returned by findInstructions.
offset := 0
if ref+instructionOffset < 0 {
offset = -(ref + instructionOffset)
}
// Figure out the index to slice at.
startIdx := ref + instructionOffset
endIdx := ref + instructionOffset + count
if endIdx <= 0 || startIdx >= len(procInstructions) {
return []proc.AsmInstruction{}, 0, nil
}
// Adjust start and end to be inbounds.
if startIdx < 0 {
offset = -startIdx
startIdx = 0
}
if endIdx > len(procInstructions) {
endIdx = len(procInstructions)
}
return procInstructions[startIdx:endIdx], offset, nil
}
func getValidRange(bi *proc.BinaryInfo) (uint64, uint64) {
return bi.Functions[0].Entry, bi.Functions[len(bi.Functions)-1].End
}
func alignPCs(bi *proc.BinaryInfo, start, end uint64) (uint64, uint64) {
// We want to find the function locations position that would enclose
// the range from start to end.
//
// Example:
//
// 0x0000 instruction (func1)
// 0x0004 instruction (func1)
// 0x0008 instruction (func1)
// 0x000c nop
// 0x000e nop
// 0x0000 nop
// 0x0002 nop
// 0x0004 instruction (func2)
// 0x0008 instruction (func2)
// 0x000c instruction (func2)
//
// start values:
// < 0x0000 at func1.Entry = 0x0000
// 0x0000-0x000b at func1.Entry = 0x0000
// 0x000c-0x0003 at func1.End = 0x000c
// 0x0004-0x000f at func2.Entry = 0x0004
// > 0x000f at func2.End = 0x0010
//
// end values:
// < 0x0000 at func1.Entry = 0x0000
// 0x0000-0x000b at func1.End = 0x0000
// 0x000c-0x0003 at func2.Entry = 0x000c
// 0x0004-0x000f at func2.End = 0x0004
// > 0x000f at func2.End = 0x0004
// Handle start values:
fn := bi.PCToFunc(start)
if fn != nil {
// start is in a function.
start = fn.Entry
} else if b, pc := checkOutOfAddressSpace(start, bi); b {
start = pc
} else {
// Otherwise it must come after some function.
i := sort.Search(len(bi.Functions), func(i int) bool {
fn := bi.Functions[len(bi.Functions)-(i+1)]
return start >= fn.End
})
start = bi.Functions[len(bi.Functions)-(i+1)].Entry
}
// Handle end values:
if fn := bi.PCToFunc(end); fn != nil {
// end is in a function.
end = fn.End
} else if b, pc := checkOutOfAddressSpace(end, bi); b {
end = pc
} else {
// Otherwise it must come before some function.
i := sort.Search(len(bi.Functions), func(i int) bool {
fn := bi.Functions[i]
return end < fn.Entry
})
end = bi.Functions[i].Entry
const limit = 10 * 1024
if end-start > limit {
end = start + limit
}
}
return start, end
}
func checkOutOfAddressSpace(pc uint64, bi *proc.BinaryInfo) (bool, uint64) {
entry, end := getValidRange(bi)
if pc < entry {
return true, entry
}
if pc >= end {
return true, end
}
return false, pc
}
// onCancelRequest sends a not-yet-implemented error response.
// Capability 'supportsCancelRequest' is not set 'initialize' response.
func (s *Session) onCancelRequest(request *dap.CancelRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onExceptionInfoRequest handles 'exceptionInfo' requests.
// Capability 'supportsExceptionInfoRequest' is set in 'initialize' response.
func (s *Session) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
goroutineID := int64(request.Arguments.ThreadId)
var body dap.ExceptionInfoResponseBody
// Get the goroutine and the current state.
g, err := s.debugger.FindGoroutine(goroutineID)
if err != nil {
s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", err.Error())
return
}
if g == nil {
s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", fmt.Sprintf("could not find goroutine %d", goroutineID))
return
}
var bpState *proc.BreakpointState
if g.Thread != nil {
bpState = g.Thread.Breakpoint()
}
// Check if this goroutine ID is stopped at a breakpoint.
includeStackTrace := true
if bpState != nil && bpState.Breakpoint != nil && bpState.Breakpoint.Logical != nil && (bpState.Breakpoint.Logical.Name == proc.FatalThrow || bpState.Breakpoint.Logical.Name == proc.UnrecoveredPanic) {
switch bpState.Breakpoint.Logical.Name {
case proc.FatalThrow:
body.ExceptionId = "fatal error"
body.Description, err = s.throwReason(goroutineID)
if err != nil {
body.Description = fmt.Sprintf("Error getting throw reason: %s", err.Error())
// This is not currently working for Go 1.16.
ver := goversion.ParseProducer(s.debugger.TargetGoVersion())
if ver.Major == 1 && ver.Minor == 16 {
body.Description = "Throw reason unavailable, see https://github.com/golang/go/issues/46425"
}
}
case proc.UnrecoveredPanic:
body.ExceptionId = "panic"
// Attempt to get the value of the panic message.
body.Description, err = s.panicReason(goroutineID)
if err != nil {
body.Description = fmt.Sprintf("Error getting panic message: %s", err.Error())
}
}
} else {
// If this thread is not stopped on a breakpoint, then a runtime error must have occurred.
// If we do not have any error saved, or if this thread is not current thread,
// return an error.
if s.exceptionErr == nil {
s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", "no runtime error found")
return
}
state, err := s.debugger.State( /*nowait*/ true)
if err != nil {
s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", err.Error())
return
}
if s.exceptionErr.Error() != "next while nexting" && (state == nil || state.CurrentThread == nil || g.Thread == nil || state.CurrentThread.ID != g.Thread.ThreadID()) {
s.sendErrorResponse(request.Request, UnableToGetExceptionInfo, "Unable to get exception info", fmt.Sprintf("no exception found for goroutine %d", goroutineID))
return
}
body.ExceptionId = "runtime error"
body.Description = s.exceptionErr.Error()
if body.Description == "bad access" {
body.Description = BetterBadAccessError
}
if body.Description == "next while nexting" {
body.ExceptionId = "invalid command"
body.Description = BetterNextWhileNextingError
includeStackTrace = false
}
}
if includeStackTrace {
body.Details = &dap.ExceptionDetails{}
frames, err := s.stacktrace(goroutineID, g)
if err != nil {
body.Details.StackTrace = fmt.Sprintf("Error getting stack trace: %s", err.Error())
} else {
body.Details.StackTrace = frames
}
}
response := &dap.ExceptionInfoResponse{
Response: *newResponse(request.Request),
Body: body,
}
s.send(response)
}
func (s *Session) stacktrace(goroutineID int64, g *proc.G) (string, error) {
frames, err := s.debugger.Stacktrace(goroutineID, s.args.StackTraceDepth, 0)
if err != nil {
return "", err
}
apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
if err != nil {
return "", err
}
var buf bytes.Buffer
fmt.Fprintln(&buf, "Stack:")
userLoc := g.UserCurrent()
userFuncPkg := fnPackageName(&userLoc)
api.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false, func(s api.Stackframe) bool {
// Include all stack frames if the stack trace is for a system goroutine,
// otherwise, skip runtime stack frames.
if userFuncPkg == "runtime" {
return true
}
return s.Location.Function != nil && !strings.HasPrefix(s.Location.Function.Name(), "runtime.")
})
return buf.String(), nil
}
func (s *Session) throwReason(goroutineID int64) (string, error) {
return s.getExprString("s", goroutineID, 0)
}
func (s *Session) panicReason(goroutineID int64) (string, error) {
return s.getExprString("(*msgs).arg.(data)", goroutineID, 0)
}
func (s *Session) getExprString(expr string, goroutineID int64, frame int) (string, error) {
exprVar, err := s.debugger.EvalVariableInScope(goroutineID, frame, 0, expr, DefaultLoadConfig)
if err != nil {
return "", err
}
if exprVar.Value == nil {
return "", exprVar.Unreadable
}
return exprVar.Value.String(), nil
}
// sendErrorResponseWithOpts offers configuration options.
//
// showUser - if true, the error will be shown to the user (e.g. via a visible pop-up)
func (s *Session) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) {
er := &dap.ErrorResponse{}
er.Type = "response"
er.Command = request.Command
er.RequestSeq = request.Seq
er.Success = false
er.Message = summary
er.Body.Error = &dap.ErrorMessage{
Id: id,
Format: fmt.Sprintf("%s: %s", summary, details),
ShowUser: showUser,
}
s.config.log.Debug(er.Body.Error.Format)
s.send(er)
}
// sendErrorResponse sends an error response with showUser disabled (default).
func (s *Session) sendErrorResponse(request dap.Request, id int, summary, details string) {
s.sendErrorResponseWithOpts(request, id, summary, details, false /*showUser*/)
}
// sendShowUserErrorResponse sends an error response with showUser enabled.
func (s *Session) sendShowUserErrorResponse(request dap.Request, id int, summary, details string) {
s.sendErrorResponseWithOpts(request, id, summary, details, true /*showUser*/)
}
// sendInternalErrorResponse sends an "internal error" response back to the client.
// We only take a seq here because we don't want to make assumptions about the
// kind of message received by the server that this error is a reply to.
func (s *Session) sendInternalErrorResponse(seq int, details string) {
er := &dap.ErrorResponse{}
er.Type = "response"
er.RequestSeq = seq
er.Success = false
er.Message = "Internal Error"
er.Body.Error = &dap.ErrorMessage{
Id: InternalError,
Format: fmt.Sprintf("%s: %s", er.Message, details),
}
s.config.log.Debug(er.Body.Error.Format)
s.send(er)
}
func (s *Session) sendUnsupportedErrorResponse(request dap.Request) {
s.sendErrorResponse(request, UnsupportedCommand, "Unsupported command",
fmt.Sprintf("cannot process %q request", request.Command))
}
func (s *Session) sendNotYetImplementedErrorResponse(request dap.Request) {
s.sendErrorResponse(request, NotYetImplemented, "Not yet implemented",
fmt.Sprintf("cannot process %q 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,
}
}
const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]
Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)`
const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint.
Use 'Continue' to resume the original step command.`
func (s *Session) resetHandlesForStoppedEvent() {
s.stackFrameHandles.reset()
s.variableHandles.reset()
s.exceptionErr = nil
}
func processExited(state *api.DebuggerState, err error) bool {
_, isexited := err.(proc.ErrProcessExited)
return isexited || err == nil && state.Exited
}
func (s *Session) setRunningCmd(running bool) {
s.runningMu.Lock()
defer s.runningMu.Unlock()
s.runningCmd = running
}
func (s *Session) isRunningCmd() bool {
s.runningMu.Lock()
defer s.runningMu.Unlock()
return s.runningCmd
}
func (s *Session) setHaltRequested(requested bool) {
s.haltMu.Lock()
defer s.haltMu.Unlock()
s.haltRequested = requested
}
func (s *Session) checkHaltRequested() bool {
s.haltMu.Lock()
defer s.haltMu.Unlock()
return s.haltRequested
}
// resumeOnce is a helper function to resume the execution
// of the target when the program is halted.
func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{}) (bool, *api.DebuggerState, error) {
// No other goroutines should be able to try to resume
// or halt execution while this goroutine is resuming
// execution, so we do not miss those events.
asyncSetupDone := make(chan struct{}, 1)
defer closeIfOpen(asyncSetupDone)
s.changeStateMu.Lock()
go func() {
defer s.changeStateMu.Unlock()
defer closeIfOpen(allowNextStateChange)
<-asyncSetupDone
}()
// There may have been a manual halt while the program was
// stopped. If this happened, do not resume execution of
// the program.
if s.checkHaltRequested() {
state, err := s.debugger.State(false)
return false, state, err
}
state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone)
return true, state, err
}
// runUntilStopAndNotify runs a debugger command until it stops on
// termination, error, breakpoint, etc, when an appropriate
// event needs to be sent to the client. allowNextStateChange is
// a channel that will be closed to signal that an
// asynchronous command has completed setup or was interrupted
// due to an error, so the server is ready to receive new requests.
func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange chan struct{}) {
state, err := s.runUntilStop(command, allowNextStateChange)
if s.conn.isClosed() {
s.config.log.Debugf("connection %d closed - stopping %q command", s.id, command)
return
}
if processExited(state, err) {
s.preTerminatedWG.Wait()
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
return
}
stopReason := s.debugger.StopReason()
file, line := "?", -1
if state != nil && state.CurrentThread != nil {
file, line = state.CurrentThread.File, state.CurrentThread.Line
}
s.config.log.Debugf("%q command stopped - reason %q, location %s:%d", command, stopReason, file, line)
s.resetHandlesForStoppedEvent()
stopped := &dap.StoppedEvent{Event: *newEvent("stopped")}
stopped.Body.AllThreadsStopped = true
if err == nil {
if stopReason == proc.StopManual {
if err := s.debugger.CancelNext(); err != nil {
s.config.log.Error(err)
} else {
state.NextInProgress = false
}
}
stopped.Body.ThreadId = int(stoppedGoroutineID(state))
switch stopReason {
case proc.StopNextFinished:
stopped.Body.Reason = "step"
case proc.StopManual: // triggered by halt
stopped.Body.Reason = "pause"
case proc.StopUnknown: // can happen while terminating
stopped.Body.Reason = "unknown"
case proc.StopWatchpoint:
stopped.Body.Reason = "data breakpoint"
default:
stopped.Body.Reason = "breakpoint"
goid, bp := s.stoppedOnBreakpointGoroutineID(state)
stopped.Body.ThreadId = int(goid)
if bp != nil {
switch bp.Name {
case proc.FatalThrow:
stopped.Body.Reason = "exception"
stopped.Body.Description = "fatal error"
stopped.Body.Text, _ = s.throwReason(int64(stopped.Body.ThreadId))
case proc.UnrecoveredPanic:
stopped.Body.Reason = "exception"
stopped.Body.Description = "panic"
stopped.Body.Text, _ = s.panicReason(int64(stopped.Body.ThreadId))
}
if strings.HasPrefix(bp.Name, functionBpPrefix) {
stopped.Body.Reason = "function breakpoint"
}
if strings.HasPrefix(bp.Name, instructionBpPrefix) {
stopped.Body.Reason = "instruction breakpoint"
}
stopped.Body.HitBreakpointIds = []int{bp.ID}
}
}
// Override the stop reason if there was a manual stop request.
// TODO(suzmue): move this logic into the runUntilStop command
// so that the stop reason is determined by that function which
// has all the context.
if stopped.Body.Reason != "exception" && s.checkHaltRequested() {
s.config.log.Debugf("manual halt requested, stop reason %q converted to \"pause\"", stopped.Body.Reason)
stopped.Body.Reason = "pause"
stopped.Body.HitBreakpointIds = []int{}
}
} else {
s.exceptionErr = err
s.config.log.Error("runtime error: ", err)
stopped.Body.Reason = "exception"
stopped.Body.Description = "runtime error"
stopped.Body.Text = err.Error()
// Special case in the spirit of https://github.com/microsoft/vscode-go/issues/1903
if stopped.Body.Text == "bad access" {
stopped.Body.Text = BetterBadAccessError
}
if stopped.Body.Text == "next while nexting" {
stopped.Body.Description = "invalid command"
stopped.Body.Text = BetterNextWhileNextingError
s.logToConsole(fmt.Sprintf("%s: %s", stopped.Body.Description, stopped.Body.Text))
}
state, err := s.debugger.State( /*nowait*/ true)
if err == nil {
stopped.Body.ThreadId = int(stoppedGoroutineID(state))
}
}
// NOTE: If we happen to be responding to another request with an is-running
// error while this one completes, it is possible that the error response
// will arrive after this stopped event.
s.send(stopped)
// Send an output event with more information if next is in progress.
if state != nil && state.NextInProgress {
s.logToConsole("Step interrupted by a breakpoint. Use 'Continue' to resume the original step command.")
}
}
func (s *Session) runUntilStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
// Clear any manual stop requests that came in before we started running.
s.setHaltRequested(false)
s.setRunningCmd(true)
defer s.setRunningCmd(false)
var state *api.DebuggerState
var err error
for s.isRunningCmd() {
state, err = resumeOnceAndCheckStop(s, command, allowNextStateChange)
command = api.DirectionCongruentContinue
}
return state, err
}
// Make this a var so it can be stubbed in testing.
var resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
return s.resumeOnceAndCheckStop(command, allowNextStateChange)
}
func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
resumed, state, err := s.resumeOnce(command, allowNextStateChange)
// We should not try to process the log points if the program was not
// resumed or there was an error.
if !resumed || processExited(state, err) || state == nil || err != nil || s.conn.isClosed() {
s.setRunningCmd(false)
return state, err
}
s.handleLogPoints(state)
gsOnBp := s.stoppedGs(state)
switch s.debugger.StopReason() {
case proc.StopBreakpoint, proc.StopManual:
// Make sure a real manual stop was requested or a real breakpoint was hit.
if len(gsOnBp) > 0 || s.checkHaltRequested() {
s.setRunningCmd(false)
}
default:
s.setRunningCmd(false)
}
// Stepping a single instruction will never require continuing again.
if command == api.StepInstruction || command == api.ReverseStepInstruction {
s.setRunningCmd(false)
}
return state, err
}
func (s *Session) handleLogPoints(state *api.DebuggerState) {
for _, th := range state.Threads {
if bp := th.Breakpoint; bp != nil {
s.logBreakpointMessage(bp, th.GoroutineID)
}
}
}
func (s *Session) stoppedGs(state *api.DebuggerState) (gs []int64) {
// Check the current thread first. There may be no selected goroutine.
if state.CurrentThread.Breakpoint != nil && !state.CurrentThread.Breakpoint.Tracepoint {
gs = append(gs, state.CurrentThread.GoroutineID)
}
if s.debugger.StopReason() == proc.StopHardcodedBreakpoint {
gs = append(gs, stoppedGoroutineID(state))
}
for _, th := range state.Threads {
// Some threads may be stopped on a hardcoded breakpoint.
// TODO(suzmue): This is a workaround for detecting hard coded breakpoints,
// though this check is likely not sufficient. It would be better to resolve
// this in the debugger layer instead.
if th.Function.Name() == "runtime.breakpoint" {
gs = append(gs, th.GoroutineID)
continue
}
// We already added the current thread if it had a breakpoint.
if th.ID == state.CurrentThread.ID {
continue
}
if bp := th.Breakpoint; bp != nil {
if !th.Breakpoint.Tracepoint {
gs = append(gs, th.GoroutineID)
}
}
}
return gs
}
func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int64) bool {
if !bp.Tracepoint {
return false
}
if lMsg, ok := bp.UserData.(logMessage); ok {
msg := lMsg.evaluate(s, goid)
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Category: "stdout",
Output: fmt.Sprintf("> [Go %d]: %s\n", goid, msg),
Source: &dap.Source{
Path: s.toClientPath(bp.File),
},
Line: bp.Line,
},
})
}
return true
}
func (msg *logMessage) evaluate(s *Session, goid int64) string {
evaluated := make([]interface{}, len(msg.args))
for i := range msg.args {
exprVar, err := s.debugger.EvalVariableInScope(goid, 0, 0, msg.args[i], DefaultLoadConfig)
if err != nil {
evaluated[i] = fmt.Sprintf("{eval err: %e}", err)
continue
}
evaluated[i], _ = s.convertVariableWithOpts(exprVar, "", skipRef|showFullValue)
}
return fmt.Sprintf(msg.format, evaluated...)
}
func (s *Session) toClientPath(path string) string {
if len(s.args.substitutePathServerToClient) == 0 {
return path
}
clientPath := locspec.SubstitutePath(path, s.args.substitutePathServerToClient)
if clientPath != path {
s.config.log.Debugf("server path=%s converted to client path=%s\n", path, clientPath)
}
return clientPath
}
func (s *Session) toServerPath(path string) string {
if len(s.args.substitutePathClientToServer) == 0 {
return path
}
serverPath := locspec.SubstitutePath(path, s.args.substitutePathClientToServer)
if serverPath != path {
s.config.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
}
return serverPath
}
type logMessage struct {
format string
args []string
}
// parseLogPoint parses a log message according to the DAP spec:
//
// "Expressions within {} are interpolated."
func parseLogPoint(msg string) (bool, *logMessage, error) {
// Note: All braces *must* come in pairs, even those within an
// expression to be interpolated.
// TODO(suzmue): support individual braces in string values in
// eval expressions.
var args []string
var isArg bool
var formatSlice, argSlice []rune
braceCount := 0
for _, r := range msg {
if isArg {
switch r {
case '}':
if braceCount--; braceCount == 0 {
argStr := strings.TrimSpace(string(argSlice))
if len(argStr) == 0 {
return false, nil, fmt.Errorf("empty evaluation string")
}
args = append(args, argStr)
formatSlice = append(formatSlice, '%', 's')
isArg = false
continue
}
case '{':
braceCount += 1
}
argSlice = append(argSlice, r)
continue
}
switch r {
case '}':
return false, nil, fmt.Errorf("invalid log point format, unexpected '}'")
case '{':
if braceCount++; braceCount == 1 {
isArg, argSlice = true, []rune{}
continue
}
}
formatSlice = append(formatSlice, r)
}
if isArg {
return false, nil, fmt.Errorf("invalid log point format")
}
if len(formatSlice) == 0 {
return false, nil, nil
}
return true, &logMessage{
format: string(formatSlice),
args: args,
}, nil
}