4024 lines
146 KiB
Go
4024 lines
146 KiB
Go
// 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 target 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 a 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[stackFrame]
|
|
// variableHandles maps compound variables to unique references within their stack frame.
|
|
// Reset at every stop.
|
|
// See also comment for convertVariable.
|
|
variableHandles *handlesMap[*fullyQualifiedVariable]
|
|
// 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 program'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 {
|
|
mu sync.Mutex
|
|
closed bool
|
|
closedChan chan struct{}
|
|
io.ReadWriteCloser
|
|
}
|
|
|
|
func newConnection(conn io.ReadWriteCloser) *connection {
|
|
return &connection{ReadWriteCloser: conn, closedChan: make(chan struct{})}
|
|
}
|
|
|
|
func (c *connection) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if !c.closed {
|
|
close(c.closedChan)
|
|
}
|
|
c.closed = true
|
|
return c.ReadWriteCloser.Close()
|
|
}
|
|
|
|
func (c *connection) isClosed() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.closed
|
|
}
|
|
|
|
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"`
|
|
// ShowPprofLabels is an array of keys of pprof labels to show as a
|
|
// goroutine name in the threads view. If the array has one element, only
|
|
// that label's value will be shown; otherwise, each of the labels will be
|
|
// shown as "key:value". To show all labels, specify the single element "*".
|
|
ShowPprofLabels []string `cfgName:"showPprofLabels"`
|
|
// 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: "",
|
|
ShowPprofLabels: []string{},
|
|
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
|
|
)
|
|
|
|
// Max number of goroutines that we will return.
|
|
// This is a var for testing
|
|
var 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.Error("Session must be configured with StopTriggered")
|
|
os.Exit(1)
|
|
}
|
|
return &Session{
|
|
config: config,
|
|
id: sessionCount,
|
|
conn: newConnection(conn),
|
|
stackFrameHandles: newHandlesMap[stackFrame](),
|
|
variableHandles: newHandlesMap[*fullyQualifiedVariable](),
|
|
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
|
|
s.args.ShowPprofLabels = args.ShowPprofLabels
|
|
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 shut down 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.Error("Misconfigured server: no Listener is configured.")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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.Error("RunWithClient must not be used when the Server is configured with a Listener")
|
|
os.Exit(1)
|
|
}
|
|
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().
|
|
var triggerServerStop bool
|
|
defer s.conn.Close()
|
|
defer func() {
|
|
if triggerServerStop {
|
|
s.config.triggerServerStop()
|
|
}
|
|
}()
|
|
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:
|
|
triggerServerStop = !s.config.AcceptMulti
|
|
if err != io.EOF { // EOF means client closed connection
|
|
var decodeErr *dap.DecodeProtocolMessageFieldError
|
|
if errors.As(err, &decodeErr) {
|
|
// 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 it's 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 target 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 := newSyncflag()
|
|
|
|
switch request := request.(type) {
|
|
//--- Asynchronous requests ---
|
|
case *dap.ConfigurationDoneRequest: // Optional (capability 'supportsConfigurationDoneRequest')
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onConfigurationDoneRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.ContinueRequest: // Required
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onContinueRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.NextRequest: // Required
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onNextRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.StepInRequest: // Required
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onStepInRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.StepOutRequest: // Required
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onStepOutRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.StepBackRequest: // Optional (capability 'supportsStepBack')
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onStepBackRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
case *dap.ReverseContinueRequest: // Optional (capability 'supportsStepBack')
|
|
go func() {
|
|
defer s.recoverPanic(request)
|
|
s.onReverseContinueRequest(request, resumeRequestLoop)
|
|
}()
|
|
resumeRequestLoop.wait()
|
|
//--- 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
|
|
}
|
|
|
|
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 a 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.value)
|
|
case "test":
|
|
cmd, out, err = gobuild.GoTestBuildCombinedOutput(args.Output, []string{args.Program}, args.BuildFlags.value)
|
|
}
|
|
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))
|
|
|
|
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 n > 0 {
|
|
outs := string(out[:n])
|
|
s.send(&dap.OutputEvent{
|
|
Event: *newEvent("output"),
|
|
Body: dap.OutputEventBody{
|
|
Output: outs,
|
|
Category: category,
|
|
},
|
|
})
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return
|
|
}
|
|
s.config.log.Errorf("failed read by %s - %v ", category, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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 closeAll 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])
|
|
closeAll = 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 closeAll != nil {
|
|
closeAll()
|
|
}
|
|
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, errors.New("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.Stdout, 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 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 errors.Is(err, proc.ErrProcessDetached) {
|
|
s.config.log.Debug("halt returned error: ", err)
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
var errProcessExited proc.ErrProcessExited
|
|
switch {
|
|
case errors.As(err, &errProcessExited):
|
|
exited = errProcessExited
|
|
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 {
|
|
var errProcessExited proc.ErrProcessExited
|
|
switch {
|
|
case errors.As(err, &errProcessExited):
|
|
s.config.log.Debug(errProcessExited)
|
|
if exited != nil {
|
|
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, 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 = errors.New("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 = errors.New("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, errors.New("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) > 1 {
|
|
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 *syncflag) {
|
|
defer allowNextStateChange.raise()
|
|
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 *syncflag) {
|
|
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 "???"
|
|
}
|
|
fullSymbol := loc.Fn.Name
|
|
packagePath := loc.Fn.PackageName()
|
|
lastSlash := strings.LastIndex(packagePath, "/")
|
|
if lastSlash >= 0 {
|
|
// strip everything until the last slash from the package path
|
|
return fullSymbol[lastSlash+1:]
|
|
}
|
|
|
|
// We either have no package name at all, or it doesn't contain a slash:
|
|
// return name unchanged
|
|
return fullSymbol
|
|
}
|
|
|
|
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 {
|
|
var errProcessExited proc.ErrProcessExited
|
|
switch {
|
|
case errors.As(err, &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 error returned in this case.
|
|
s.config.log.Debug(errProcessExited)
|
|
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())
|
|
}
|
|
var labels strings.Builder
|
|
writeLabelsForKeys := func(keys []string) {
|
|
for _, k := range keys {
|
|
labelValue := g.Labels()[k]
|
|
if labelValue != "" {
|
|
labels.WriteByte(' ')
|
|
labels.WriteString(k)
|
|
labels.WriteByte(':')
|
|
labels.WriteString(labelValue)
|
|
}
|
|
}
|
|
}
|
|
if len(s.args.ShowPprofLabels) == 1 {
|
|
labelKey := s.args.ShowPprofLabels[0]
|
|
if labelKey == "*" {
|
|
keys := make([]string, 0, len(g.Labels()))
|
|
for k := range g.Labels() {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
writeLabelsForKeys(keys)
|
|
} else {
|
|
labelValue := g.Labels()[labelKey]
|
|
if labelValue != "" {
|
|
labels.WriteByte(' ')
|
|
labels.WriteString(labelValue)
|
|
}
|
|
}
|
|
} else {
|
|
writeLabelsForKeys(s.args.ShowPprofLabels)
|
|
}
|
|
// 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%s", selected, g.ID, labels.String(), 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) {
|
|
args := 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.AttachWaitFor != "" && args.ProcessID != 0 {
|
|
s.sendShowUserErrorResponse(
|
|
request.Request,
|
|
FailedToAttach,
|
|
"Failed to attach",
|
|
"'processId' and 'waitFor' are mutually exclusive, and can't be specified at the same time")
|
|
return
|
|
} else if args.AttachWaitFor != "" {
|
|
s.config.Debugger.AttachWaitFor = args.AttachWaitFor
|
|
// Keep the default value the same as waitfor-interval.
|
|
s.config.Debugger.AttachWaitForInterval = 1
|
|
s.config.log.Debugf("Waiting for a process with a name beginning with this prefix: %s", args.AttachWaitFor)
|
|
} else if args.ProcessID != 0 {
|
|
s.config.Debugger.AttachPid = args.ProcessID
|
|
s.config.log.Debugf("Attaching to pid %d", args.ProcessID)
|
|
} else {
|
|
s.sendShowUserErrorResponse(
|
|
request.Request,
|
|
FailedToAttach,
|
|
"Failed to attach",
|
|
"The 'processId' or 'waitFor' attribute is missing in debug configuration")
|
|
return
|
|
}
|
|
if args.Backend == "" {
|
|
args.Backend = "default"
|
|
}
|
|
s.config.Debugger.Backend = args.Backend
|
|
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)
|
|
|
|
if len(args.LaunchAttachCommonConfig.SubstitutePath) == 0 && args.GuessSubstitutePath != nil && s.debugger != nil {
|
|
server2Client := s.debugger.GuessSubstitutePath(args.GuessSubstitutePath)
|
|
clientToServer := make([][2]string, 0, len(server2Client))
|
|
serverToClient := make([][2]string, 0, len(server2Client))
|
|
for serverDir, clientDir := range server2Client {
|
|
serverToClient = append(serverToClient, [2]string{serverDir, clientDir})
|
|
clientToServer = append(clientToServer, [2]string{clientDir, serverDir})
|
|
}
|
|
s.args.substitutePathClientToServer = clientToServer
|
|
s.args.substitutePathServerToClient = serverToClient
|
|
}
|
|
|
|
// 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 *syncflag) {
|
|
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 *syncflag) {
|
|
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 *syncflag) {
|
|
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 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, bp.Breakpoint.Logical, []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 *syncflag) {
|
|
defer allowNextStateChange.raise()
|
|
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil, s.conn.closedChan)
|
|
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.goroutineID
|
|
frame := sf.frameIndex
|
|
|
|
// Check if the function is optimized.
|
|
fn, err := s.debugger.Function(int64(goid), frame, 0)
|
|
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)
|
|
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)
|
|
|
|
switch {
|
|
case strings.HasPrefix(c.Name, "~") || strings.HasPrefix(c.Name, "."):
|
|
cfqname = ""
|
|
case v.isScope && v.fullyQualifiedNameOrExpr == "":
|
|
cfqname = c.Name
|
|
case v.fullyQualifiedNameOrExpr == "":
|
|
cfqname = ""
|
|
case v.Kind == reflect.Interface:
|
|
cfqname = fmt.Sprintf("%s.(%s)", v.fullyQualifiedNameOrExpr, c.Name) // c is data
|
|
case v.Kind == reflect.Ptr:
|
|
cfqname = fmt.Sprintf("(*%v)", v.fullyQualifiedNameOrExpr) // c is the nameless pointer value
|
|
case 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 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 ""
|
|
}
|
|
switch v.Kind {
|
|
case reflect.Interface:
|
|
if len(v.Children) > 0 {
|
|
vapi := api.ConvertVar(v)
|
|
if vapi.Children[0].Kind != reflect.Invalid {
|
|
return fmt.Sprintf("%s(%s)", vapi.Type, vapi.Children[0].Type)
|
|
}
|
|
}
|
|
}
|
|
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).SinglelineStringWithShortTypes()
|
|
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.
|
|
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).SinglelineStringWithShortTypes()
|
|
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).SinglelineStringWithShortTypes()
|
|
}
|
|
return value
|
|
}
|
|
|
|
switch v.Kind {
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
n, _ := strconv.ParseUint(api.ExtractIntValue(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).SinglelineStringWithShortTypes()
|
|
}
|
|
} 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, reflect.Func:
|
|
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
|
|
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.goroutineID
|
|
frame = sf.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, errors.New("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, s.conn.closedChan)
|
|
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 *syncflag) {
|
|
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 *syncflag) {
|
|
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 := 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 != addr {
|
|
return nil, -1, errors.New("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, api.StackTraceColors{}, 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 {
|
|
var errProcessExited proc.ErrProcessExited
|
|
isexited := errors.As(err, &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 *syncflag) (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 allowNextStateChange.raise()
|
|
<-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, s.conn.closedChan)
|
|
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 *syncflag) {
|
|
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 *syncflag) (*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 *syncflag) (*api.DebuggerState, error) {
|
|
return s.resumeOnceAndCheckStop(command, allowNextStateChange)
|
|
}
|
|
|
|
func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange *syncflag) (*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, errors.New("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, errors.New("invalid log point format, unexpected '}'")
|
|
case '{':
|
|
if braceCount++; braceCount == 1 {
|
|
isArg, argSlice = true, []rune{}
|
|
continue
|
|
}
|
|
}
|
|
formatSlice = append(formatSlice, r)
|
|
}
|
|
if isArg {
|
|
return false, nil, errors.New("invalid log point format")
|
|
}
|
|
if len(formatSlice) == 0 {
|
|
return false, nil, nil
|
|
}
|
|
return true, &logMessage{
|
|
format: string(formatSlice),
|
|
args: args,
|
|
}, nil
|
|
}
|
|
|
|
type syncflag struct {
|
|
mu sync.Mutex
|
|
cond *sync.Cond
|
|
flag bool
|
|
}
|
|
|
|
func newSyncflag() *syncflag {
|
|
r := &syncflag{
|
|
flag: false,
|
|
}
|
|
r.cond = sync.NewCond(&r.mu)
|
|
return r
|
|
}
|
|
|
|
func (s *syncflag) wait() {
|
|
s.mu.Lock()
|
|
for !s.flag {
|
|
s.cond.Wait()
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *syncflag) raise() {
|
|
s.mu.Lock()
|
|
s.flag = true
|
|
s.mu.Unlock()
|
|
s.cond.Broadcast()
|
|
}
|