// 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" "io/ioutil" "net" "os" "os/exec" "path/filepath" "reflect" "regexp" "runtime" "runtime/debug" "strconv" "strings" "sync" "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" "github.com/sirupsen/logrus" ) // 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.DisconnecChan when encountering a client connection // error or responding to a (synchronous) DAP disconnect request. // Once stop is triggered, the goroutine exits. // // Unlike rpccommon, there is not another layer of per-client // goroutines here because the dap server does not support // multiple clients. // // (3) Per-request goroutine is started for each asynchronous request // that resumes execution. We check if target is running already, so // there should be no more than one pending asynchronous request at // a time. This goroutine issues commands to the underlying debugger // and sends back events and responses. It takes a setup-done channel // as an argument and temporarily blocks the request loop until setup // for asynchronous execution is complete and targe is running. // Once done, it unblocks processing of parallel requests unblocks // (e.g. disconnecting while the program is running). // // These per-request goroutines never send a stop-server signal. // They block on running debugger commands that are interrupted // when halt is issued while stopping. At that point these goroutines // wrap-up and exit. type Server struct { // config is all the information necessary to start the debugger and server. config *Config // listener is used to accept the client connection. listener net.Listener // session is the debug session that comes with an client connection. session *Session sessionMu sync.Mutex } // Session is an abstraction for serving and shutting down // a DAP debug session with a pre-connected client. // TODO(polina): move this to a different file/package type Session struct { config *Config // stackFrameHandles maps frames of each goroutine to unique ids across all goroutines. // Reset at every stop. stackFrameHandles *handlesMap // variableHandles maps compound variables to unique references within their stack frame. // Reset at every stop. // See also comment for convertVariable. variableHandles *variablesHandlesMap // args tracks special settings for handling debug session requests. args launchAttachArgs // exceptionErr tracks the runtime error that last occurred. exceptionErr error // clientCapabilities tracks special settings for handling debug session requests. clientCapabilities dapClientCapabilites // 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 io.ReadWriteCloser // 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 *exec.Cmd // sendingMu synchronizes writing to conn // to ensure that messages do not get interleaved sendingMu sync.Mutex // runningCmd tracks whether the server is running an asyncronous // 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 } // 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 *logrus.Entry // stopTriggered is closed when the server is Stop()-ed. // Can be used to safeguard against duplicate shutdown sequences. stopTriggered chan struct{} } // launchAttachArgs captures arguments from launch/attach request that // impact handling of subsequent requests. type launchAttachArgs struct { // stopOnEntry is set to automatically stop the debugee after start. stopOnEntry bool // stackTraceDepth is the maximum length of the returned list of stack frames. stackTraceDepth int // showGlobalVariables indicates if global package variables should be loaded. showGlobalVariables bool // substitutePathClientToServer indicates rules for converting file paths between client and debugger. // These must be directory paths. substitutePathClientToServer [][2]string // substitutePathServerToClient indicates rules for converting file paths between debugger and client. // These must be directory paths. 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, substitutePathClientToServer: [][2]string{}, substitutePathServerToClient: [][2]string{}, } // dapClientCapabilites captures arguments from intitialize request that // impact handling of subsequent requests. type dapClientCapabilites 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 performace overhead for bulk loading. // With dlv-dap, users do not have a way to adjust these. // Instead we are focusing in interacive loading with nested reloads, array/map // paging and context-specific string limits. var DefaultLoadConfig = proc.LoadConfig{ FollowPointers: true, MaxVariableRecurse: 1, // TODO(polina): consider 1024 limit instead: // - vscode+C appears to use 1024 as the load limit // - vscode viewlet hover truncates at 1023 characters MaxStringLen: 512, MaxArrayValues: 64, MaxStructFields: -1, } const ( // When a user examines a single string, we can relax the loading limit. maxSingleStringLen = 4 << 10 // 4096 // Results of a call are single-use and transient. We need to maximize // what is presented. A common use case of a call injection is to // stringify complex data conveniently. maxStringLenInCallRetVars = 1 << 10 // 1024 ) var ( // Max number of goroutines that we will return. // This is a var for testing maxGoroutines = 1 << 10 ) // NewServer creates a new DAP Server. It takes an opened Listener // via config and assumes its ownership. config.DisconnectChan has to be set; // it will be closed by the server when the client fails to connect, // disconnects or requests shutdown. Once config.DisconnectChan is closed, // Server.Stop() must be called to shutdown this single-user server. func NewServer(config *service.Config) *Server { logger := logflags.DAPLogger() logflags.WriteDAPListeningMessage(config.Listener.Addr()) 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, } } // 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) *Session { if config.log == nil { config.log = logflags.DAPLogger() } config.log.Debug("DAP connection started") return &Session{ config: config, conn: conn, stackFrameHandles: newHandlesMap(), variableHandles: newVariablesHandlesMap(), args: defaultArgs, exceptionErr: nil, } } // If user-specified options are provided via Launch/AttachRequest, // we override the defaults for optional args. func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error { s.args.stopOnEntry = args.StopOnEntry if depth := args.StackTraceDepth; depth > 0 { s.args.stackTraceDepth = depth } s.args.showGlobalVariables = args.ShowGlobalVariables 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 } return nil } // 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 progreess. func (s *Server) Stop() { s.config.log.Debug("DAP server stopping...") defer s.config.log.Debug("DAP server stopped") close(s.config.stopTriggered) // 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. func (s *Session) Close() { s.mu.Lock() defer s.mu.Unlock() if s.debugger != nil { killProcess := s.config.Debugger.AttachPid == 0 s.stopDebugSession(killProcess) } else { 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) } // 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 andd // allowing the run goroutinee to exit. _ = 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 agaist closing the channel more // than once and can be called multiple times. It is not thread-safe // and is currently only called from the run goroutine. func (c *Config) triggerServerStop() { // Avoid accidentally closing the channel twice and causing a panic, when // this function is called more than once. For example, we could have the // following sequence of events: // -- run goroutine: calls onDisconnectRequest() // -- run goroutine: calls triggerServerStop() // -- main goroutine: calls Stop() // -- main goroutine: Stop() closes client connection (or client closed it) // -- run goroutine: serveDAPCodec() gets "closed network connection" // -- run goroutine: serveDAPCodec() returns and calls triggerServerStop() if c.DisconnectChan != nil { close(c.DisconnectChan) c.DisconnectChan = nil } // There should be no logic here after the stop-server // signal that might cause everything to shutdown before this // logic gets executed. } // Run launches a new goroutine where it accepts a client connection // and starts processing requests from it. Use Stop() to close connection. // The server does not support multiple clients, serially or in parallel. // The server should be restarted for every new debug session. // The debugger won't be started until launch/attach request is received. // TODO(polina): allow new client connections for new debug sessions, // so the editor needs to launch dap server only once? Note that some requests // may change the server's environment (e.g. see dlvCwd of launch configuration). // So if we want to reuse this server for multiple independent debugging sessions // we need to take that into consideration. func (s *Server) Run() { 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.sessionMu.Lock() s.session = NewSession(conn, s.config) // closed in Stop() s.sessionMu.Unlock() s.session.serveDAPCodec() }() } // 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() { // TODO(polina): defer-close conn/session like in serveJSONCodec reader := bufio.NewReader(s.conn) for { request, err := dap.ReadProtocolMessage(reader) // Handle dap.DecodeProtocolMessageFieldError errors gracefully by responding with an ErrorResponse. // For example: // -- "Request command 'foo' is not supported" means we // potentially got some new DAP request that we do not yet have // decoding support for, so we can respond with an ErrorResponse. // // Other errors, such as unmarshalling errors, will log the error and cause the server to trigger // a stop. if err != nil { s.config.log.Debug("DAP error: ", err) select { case <-s.config.stopTriggered: default: if err != io.EOF { // EOF means client closed connection if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok { // Send an error response to the users if we were unable to process the message. s.sendInternalErrorResponse(decodeErr.Seq, err.Error()) continue } s.config.log.Error("DAP error: ", err) } if s.config.AcceptMulti { s.conn.Close() } else { s.config.triggerServerStop() } } return } s.handleRequest(request) } } // In case a handler panics, we catch the panic to avoid crashing both // the server and the target. We send an error response back, but // in case its a dup and ignored by the client, we also log the error. func (s *Session) recoverPanic(request dap.Message) { if ierr := recover(); ierr != nil { s.config.log.Errorf("recovered panic: %s\n%s\n", ierr, debug.Stack()) s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("%v", ierr)) } } func (s *Session) handleRequest(request dap.Message) { defer s.recoverPanic(request) jsonmsg, _ := json.Marshal(request) s.config.log.Debug("[<- from client]", string(jsonmsg)) if _, ok := request.(dap.RequestMessage); !ok { s.sendInternalErrorResponse(request.GetSeq(), fmt.Sprintf("Unable to process non-request %#v\n", request)) return } if s.isNoDebug() { switch request := request.(type) { case *dap.DisconnectRequest: s.onDisconnectRequest(request) case *dap.RestartRequest: s.sendUnsupportedErrorResponse(request.Request) default: r := request.(dap.RequestMessage).GetRequest() s.sendErrorResponse(*r, NoDebugIsRunning, "noDebug mode", fmt.Sprintf("unable to process '%s' request", r.Command)) } return } // These requests, can be handled regardless of whether the targret is running switch request := request.(type) { case *dap.DisconnectRequest: // Required s.onDisconnectRequest(request) return case *dap.PauseRequest: // Required s.onPauseRequest(request) return case *dap.TerminateRequest: // Optional (capability ‘supportsTerminateRequest‘) // TODO: implement this request in V1 s.onTerminateRequest(request) return case *dap.RestartRequest: // Optional (capability ‘supportsRestartRequest’) // TODO: implement this request in V1 s.onRestartRequest(request) 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.isRunningCmd() { switch request := request.(type) { case *dap.ThreadsRequest: // 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: 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.logToConsole("Execution halted to set breakpoints - please resume execution manually") s.onSetBreakpointsRequest(request) // TODO(polina): consider resuming execution here automatically after suppressing // a stop event when an operation in runUntilStopAndNotify returns. In case that operation // was already stopping for a different reason, we would need to examine the state // that is returned to determine if this halt was the cause of the stop or not. // We should stop with an event and not resume if one of the following is true: // - StopReason is anything but manual // - Any thread has a breakpoint or CallReturn set // - NextInProgress is false and the last command sent by the user was: next, // step, stepOut, reverseNext, reverseStep or reverseStepOut // Otherwise, we can skip the stop event and resume the temporarily // interrupted process execution with api.DirectionCongruentContinue. // For this to apply in cases other than api.Continue, we would also need to // introduce a new version of halt that skips ClearInternalBreakpoints // in proc.(*Target).Continue, leaving NextInProgress as true. case *dap.SetFunctionBreakpointsRequest: 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.logToConsole("Execution halted to set breakpoints - please resume execution manually") s.onSetFunctionBreakpointsRequest(request) default: r := request.(dap.RequestMessage).GetRequest() s.sendErrorResponse(*r, DebuggeeIsRunning, fmt.Sprintf("Unable to process `%s`", r.Command), "debuggee is running") } return } // Requests below can only be handled while target is stopped. // Some of them are blocking and will be handled synchronously // on this goroutine while non-blocking requests will be dispatched // to another goroutine. Please note that because of the running // check above, there should be no more than one pending asynchronous // request at a time. // Non-blocking request handlers will signal when they are ready // setting up for async execution, so more requests can be processed. resumeRequestLoop := make(chan struct{}) switch request := request.(type) { //--- Asynchronous requests --- case *dap.ConfigurationDoneRequest: // Optional (capability ‘supportsConfigurationDoneRequest’) go func() { defer s.recoverPanic(request) s.onConfigurationDoneRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.ContinueRequest: // Required go func() { defer s.recoverPanic(request) s.onContinueRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.NextRequest: // Required go func() { defer s.recoverPanic(request) s.onNextRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.StepInRequest: // Required go func() { defer s.recoverPanic(request) s.onStepInRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.StepOutRequest: // Required go func() { defer s.recoverPanic(request) s.onStepOutRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.StepBackRequest: // Optional (capability ‘supportsStepBack’) go func() { defer s.recoverPanic(request) s.onStepBackRequest(request, resumeRequestLoop) }() <-resumeRequestLoop case *dap.ReverseContinueRequest: // Optional (capability ‘supportsStepBack’) go func() { defer s.recoverPanic(request) s.onReverseContinueRequest(request, resumeRequestLoop) }() <-resumeRequestLoop //--- Synchronous requests --- // TODO(polina): target might be running when remote attach debug session // is started. Support handling initialize and attach requests while running. case *dap.InitializeRequest: // Required s.onInitializeRequest(request) case *dap.LaunchRequest: // Required s.onLaunchRequest(request) case *dap.AttachRequest: // Required s.onAttachRequest(request) 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’) // Supported by vscode-go // TODO: implement this request in V0 s.onSetVariableRequest(request) case *dap.SetExpressionRequest: // Optional (capability ‘supportsSetExpression’) // TODO: implement this request in V1 s.onSetExpressionRequest(request) case *dap.LoadedSourcesRequest: // Optional (capability ‘supportsLoadedSourcesRequest’) // TODO: implement this request in V1 s.onLoadedSourcesRequest(request) case *dap.ReadMemoryRequest: // Optional (capability ‘supportsReadMemoryRequest‘) // TODO: implement this request in V1 s.onReadMemoryRequest(request) case *dap.DisassembleRequest: // Optional (capability ‘supportsDisassembleRequest’) // TODO: implement this request in V1 s.onDisassembleRequest(request) case *dap.CancelRequest: // Optional (capability ‘supportsCancelRequest’) // TODO: does this request make sense for delve? s.onCancelRequest(request) case *dap.ExceptionInfoRequest: // Optional (capability ‘supportsExceptionInfoRequest’) s.onExceptionInfoRequest(request) //--- 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.SourceRequest: // Required // This does not make sense in the context of Go as // the source cannot be a string eval'ed at runtime. 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) case *dap.ModulesRequest: // Optional (capability ‘supportsModulesRequest’) // TODO: does this request make sense for delve? 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.SupportTerminateDebuggee = 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 // TODO(polina): support these requests in addition to vscode-go feature parity response.Body.SupportsTerminateRequest = false response.Body.SupportsRestartRequest = false response.Body.SupportsStepBack = false // To be enabled by CapabilitiesEvent based on configuration response.Body.SupportsSetExpression = false response.Body.SupportsLoadedSourcesRequest = false response.Body.SupportsReadMemoryRequest = false response.Body.SupportsDisassembleRequest = 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 } // Default output file pathname for the compiled binary in debug or test modes // when temporary debug binary creation fails. // This is relative to the current working directory of the server. const defaultDebugBinary string = "./__debug_bin" func (s *Session) tempDebugBinary() string { binaryPattern := "__debug_bin" if runtime.GOOS == "windows" { binaryPattern = "__debug_bin*.exe" } f, err := ioutil.TempFile("", binaryPattern) if err != nil { s.config.log.Errorf("failed to create a temporary binary (%v), falling back to %q", err, defaultDebugBinary) return cleanExeName(defaultDebugBinary) } name := f.Name() if err := f.Close(); err != nil { s.config.log.Errorf("failed to create a temporary binary (%v), falling back to %q", err, defaultDebugBinary) return cleanExeName(defaultDebugBinary) } return name } 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) { if s.debugger != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", "debugger already started - use remote attach to connect to a server with an active debug session") return } var args = defaultLaunchConfig // narrow copy for initializing non-zero default values if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - %v", err)) return } 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 } } mode := args.Mode if mode == "" { mode = "debug" } if !isValidLaunchMode(mode) { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", mode)) return } program := args.Program if program == "" && 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 backend := args.Backend; backend != "" { s.config.Debugger.Backend = backend } else { s.config.Debugger.Backend = "default" } if mode == "replay" { traceDirPath := args.TraceDirPath // Validate trace directory if 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 = traceDirPath s.config.Debugger.Backend = "rr" } if mode == "core" { coreFilePath := args.CoreFilePath // Validate core dump path if coreFilePath == "" { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", "The 'coreFilePath' attribute is missing in debug configuration.") return } // Assign the non-empty core file path to debugger configuration. This will // trigger a native core file replay instead of an rr trace replay s.config.Debugger.CoreFile = coreFilePath s.config.Debugger.Backend = "core" } s.config.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend) // Prepare the debug executable filename, build flags and build it if mode == "debug" || mode == "test" { debugbinary := args.Output if debugbinary == "" { debugbinary = s.tempDebugBinary() } else { debugbinary = cleanExeName(debugbinary) } if o, err := filepath.Abs(debugbinary); err != nil { s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error()) return } else { debugbinary = o } buildFlags := args.BuildFlags var cmd string var out []byte wd, _ := os.Getwd() s.config.log.Debugf("building program '%s' in '%s' with flags '%v'", program, wd, buildFlags) var err error switch mode { case "debug": cmd, out, err = gobuild.GoBuildCombinedOutput(debugbinary, []string{program}, buildFlags) case "test": cmd, out, err = gobuild.GoTestBuildCombinedOutput(debugbinary, []string{program}, buildFlags) } if err != nil { 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 } program = debugbinary s.mu.Lock() s.binaryToRemove = debugbinary s.mu.Unlock() } if err := s.setLaunchAttachArgs(args.LaunchAttachCommonConfig); err != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error()) return } s.config.ProcessArgs = append([]string{program}, args.Args...) if args.Cwd != "" { s.config.Debugger.WorkingDir = args.Cwd } else if mode == "test" { // In test mode, run the test binary from the package directory // like in `go test` and `dlv test` by default. s.config.Debugger.WorkingDir = s.getPackageDir(args.Program) } else { s.config.Debugger.WorkingDir = "." } s.config.log.Debugf("running binary '%s' in '%s'", program, s.config.Debugger.WorkingDir) if args.NoDebug { s.mu.Lock() cmd, err := s.newNoDebugProcess(program, args.Args, s.config.Debugger.WorkingDir) 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 err := cmd.Wait(); err != nil { s.config.log.Debugf("program exited with error: %v", err) } stopped := false s.mu.Lock() stopped = s.noDebugProcess == nil // if it was stopped, this should be nil s.noDebugProcess = nil s.mu.Unlock() if !stopped { // process terminated on its own s.logToConsole(proc.ErrProcessExited{Pid: cmd.ProcessState.Pid(), Status: cmd.ProcessState.ExitCode()}.Error()) s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")}) } }() return } 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, s.config.ProcessArgs) }() if err != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", 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}}}) } // 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 determin 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) (*exec.Cmd, error) { if s.noDebugProcess != nil { return nil, fmt.Errorf("another launch request is in progress") } cmd := exec.Command(program, targetArgs...) cmd.Stdout, cmd.Stderr, cmd.Stdin, cmd.Dir = os.Stdout, os.Stderr, os.Stdin, wd if err := cmd.Start(); err != nil { return nil, err } s.noDebugProcess = cmd 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 } if s.noDebugProcess.ProcessState != nil && s.noDebugProcess.ProcessState.Exited() { s.logToConsole(proc.ErrProcessExited{Pid: s.noDebugProcess.ProcessState.Pid(), Status: s.noDebugProcess.ProcessState.ExitCode()}.Error()) } else { // 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. } 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.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, but not the server. s.logToConsole("Closing client session, but leaving multi-client DAP server running at " + s.config.Listener.Addr().String()) s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)}) s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")}) s.conn.Close() // 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.config.Debugger.AttachPid == 0 || request.Arguments.TerminateDebuggee err = s.stopDebugSession(killProcess) } else { s.stopNoDebugProcess() } if err != nil { s.sendErrorResponse(request.Request, DisconnectError, "Error while disconnecting", err.Error()) } else { s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)}) } // 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 s.changeStateMu.Unlock() if s.debugger == nil { return nil } // TODO(polina): reset debuggeer to nil at the end var err error var exited error // Halting will stop any debugger command that's pending on another // per-request goroutine, hence unblocking that goroutine to wrap-up and exit. // TODO(polina): Per-request goroutine could still not be done when this one is. // To avoid goroutine leaks, we can use a wait group or have the goroutine listen // for a stop signal on a dedicated quit channel at suitable points (use context?). // Additional clean-up might be especially critical when we support multiple clients. state, err := s.halt() if err == proc.ErrProcessDetached { s.config.log.Debug("halt returned error: ", err) return nil } if err != nil { switch err.(type) { case proc.ErrProcessExited: exited = err default: s.config.log.Error("halt returned error: ", err) if err.Error() == "no such process" { exited = err } } } else if state.Exited { exited = proc.ErrProcessExited{Pid: s.debugger.ProcessPid(), Status: state.ExitStatus} s.config.log.Debug("halt returned state: ", exited) } if exited != nil { // TODO(suzmue): log exited error when the process exits, which may have been before // halt was called. s.logToConsole(exited.Error()) s.logToConsole("Detaching") } else if killProcess { s.logToConsole("Detaching and terminating target process") } else { s.logToConsole("Detaching without terminating target processs") } err = s.debugger.Detach(killProcess) if err != nil { switch err.(type) { case proc.ErrProcessExited: s.config.log.Debug(err) s.logToConsole(exited.Error()) err = nil default: s.config.log.Error("detach returned error: ", err) } } return err } // halt sends a halt request if the debuggee is running. // changeStateMu should be held when calling (*Server).halt. func (s *Session) halt() (*api.DebuggerState, error) { s.setHaltRequested(true) // Only send a halt request if the debuggee is running. if s.debugger.IsRunning() { return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil) } return s.debugger.State(false) } func (s *Session) isNoDebug() bool { s.mu.Lock() defer s.mu.Unlock() return s.noDebugProcess != nil } func (s *Session) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) { if request.Arguments.Source.Path == "" { s.sendErrorResponse(request.Request, UnableToSetBreakpoints, "Unable to set or clear breakpoints", "empty file path") return } clientPath := request.Arguments.Source.Path serverPath := s.toServerPath(clientPath) // Get all existing breakpoints that match for this source. sourceRequestPrefix := fmt.Sprintf("sourceBp Path=%q ", request.Arguments.Source.Path) breakpoints := s.setBreakpoints(sourceRequestPrefix, len(request.Arguments.Breakpoints), func(i int) *bpMetadata { want := request.Arguments.Breakpoints[i] return &bpMetadata{ name: fmt.Sprintf("%s Line=%d Column=%d", sourceRequestPrefix, want.Line, want.Column), condition: want.Condition, hitCondition: want.HitCondition, logMessage: want.LogMessage, } }, func(i int) (*bpLocation, error) { want := request.Arguments.Breakpoints[i] return &bpLocation{ file: serverPath, line: want.Line, }, nil }) response := &dap.SetBreakpointsResponse{Response: *newResponse(request.Request)} response.Body.Breakpoints = breakpoints s.send(response) } type bpMetadata struct { name string condition string hitCondition string logMessage string } type bpLocation struct { file string line int addr uint64 addrs []uint64 } // setBreakpoints is a helper function for setting source, function and instruction // breakpoints. It takes the prefix of the name for all breakpoints that should be // included, the total number of breakpoints, and functions for computing the metadata // and the location. The location is computed separately because this may be more // expensive to compute and may not always be necessary. func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(i int) *bpMetadata, locFunc func(i int) (*bpLocation, error)) []dap.Breakpoint { // If a breakpoint: // -- exists and not in request => ClearBreakpoint // -- exists and in request => AmendBreakpoint // -- doesn't exist and in request => SetBreakpoint // Get all existing breakpoints matching the prefix. existingBps := s.getMatchingBreakpoints(prefix) // createdBps is a set of breakpoint names that have been added // during this request. This is used to catch duplicate set // breakpoints requests and to track which breakpoints need to // be deleted. createdBps := make(map[string]struct{}, len(existingBps)) breakpoints := make([]dap.Breakpoint, totalBps) // Amend existing breakpoints. for i := 0; i < totalBps; i++ { want := metadataFunc(i) got, ok := existingBps[want.name] if got == nil || !ok { // Skip if the breakpoint does not already exist. continue } var err error if _, ok := createdBps[want.name]; ok { err = fmt.Errorf("breakpoint already exists") } else { got.Cond = want.condition got.HitCond = want.hitCondition got.Tracepoint = want.logMessage != "" got.UserData = want.logMessage err = s.debugger.AmendBreakpoint(got) } createdBps[want.name] = struct{}{} s.updateBreakpointsResponse(breakpoints, i, err, got) } // Clear breakpoints. // Any breakpoint that existed before this request but was not amended must be deleted. s.clearBreakpoints(existingBps, createdBps) // Add new breakpoints. for i := 0; i < totalBps; i++ { want := metadataFunc(i) if _, ok := existingBps[want.name]; ok { continue } var got *api.Breakpoint wantLoc, err := locFunc(i) if err == nil { if _, ok := createdBps[want.name]; ok { err = fmt.Errorf("breakpoint already exists") } else { // Create new breakpoints. got, err = s.debugger.CreateBreakpoint( &api.Breakpoint{ Name: want.name, File: wantLoc.file, Line: wantLoc.line, Addr: wantLoc.addr, Addrs: wantLoc.addrs, Cond: want.condition, HitCond: want.hitCondition, Tracepoint: want.logMessage != "", UserData: want.logMessage, }) } } createdBps[want.name] = struct{}{} s.updateBreakpointsResponse(breakpoints, i, err, got) } return breakpoints } 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 breakpoint spec, err := locspec.Parse(want.Name) if err != nil { return nil, err } if loc, ok := spec.(*locspec.NormalLocationSpec); !ok || loc.FuncBase == nil { // Other locations do not make sense in the context of function breakpoints. // Regex locations are likely to resolve to multiple places and offset locations // are only meaningful at the time the breakpoint was created. return nil, fmt.Errorf("breakpoint name %q could not be parsed as a function. name must be in the format 'funcName', 'funcName:line' or 'fileName:line'", want.Name) } if want.Name[0] == '.' { return nil, fmt.Errorf("breakpoint names that are relative paths are not supported") } // Find the location of the function name. CreateBreakpoint requires the name to include the base // (e.g. main.functionName is supported but not functionName). // We first find the location of the function, and then set breakpoints for that location. var locs []api.Location locs, err = s.debugger.FindLocationSpec(-1, 0, 0, want.Name, spec, true, s.args.substitutePathClientToServer) if err != nil { return nil, err } if len(locs) == 0 { return nil, err } if len(locs) > 0 { s.config.log.Debugf("multiple locations found for %s", want.Name) } // Set breakpoint using the PCs that were found. loc := locs[0] return &bpLocation{addr: loc.PC, addrs: loc.PCs}, nil }) response := &dap.SetFunctionBreakpointsResponse{Response: *newResponse(request.Request)} response.Body.Breakpoints = breakpoints s.send(response) } const instructionBpPrefix = "instructionBreakpoint" func (s *Session) onSetInstructionBreakpointsRequest(request *dap.SetInstructionBreakpointsRequest) { breakpoints := s.setBreakpoints(instructionBpPrefix, len(request.Arguments.Breakpoints), func(i int) *bpMetadata { want := request.Arguments.Breakpoints[i] return &bpMetadata{ name: fmt.Sprintf("%s PC=%s", instructionBpPrefix, want.InstructionReference), condition: want.Condition, hitCondition: want.HitCondition, logMessage: "", } }, func(i int) (*bpLocation, error) { want := request.Arguments.Breakpoints[i] addr, err := strconv.ParseInt(want.InstructionReference, 0, 64) if err != nil { return nil, err } return &bpLocation{addr: uint64(addr)}, nil }) response := &dap.SetInstructionBreakpointsResponse{Response: *newResponse(request.Request)} response.Body.Breakpoints = breakpoints s.send(response) } func (s *Session) clearBreakpoints(existingBps map[string]*api.Breakpoint, amendedBps map[string]struct{}) error { for req, bp := range existingBps { if _, ok := amendedBps[req]; ok { continue } _, err := s.debugger.ClearBreakpoint(bp) if err != nil { return err } } return nil } func (s *Session) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint { existing := s.debugger.Breakpoints(false) matchingBps := make(map[string]*api.Breakpoint, len(existing)) for _, bp := range existing { // Skip special breakpoints such as for panic. if bp.ID < 0 { continue } // Skip breakpoints that do not meet the condition. if !strings.HasPrefix(bp.Name, prefix) { continue } matchingBps[bp.Name] = bp } return matchingBps } func (s *Session) onSetExceptionBreakpointsRequest(request *dap.SetExceptionBreakpointsRequest) { // Unlike what DAP documentation claims, this request is always sent // even though we specified no filters at initialization. Handle as no-op. s.send(&dap.SetExceptionBreakpointsResponse{Response: *newResponse(request.Request)}) } func closeIfOpen(ch chan struct{}) { if ch != nil { select { case <-ch: // already closed default: close(ch) } } } // onConfigurationDoneRequest handles 'configurationDone' request. // This is an optional request enabled by capability ‘supportsConfigurationDoneRequest’. // It gets triggered after all the debug requests that follow initalized event, // so the s.debugger is guaranteed to be set. func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange chan struct{}) { defer closeIfOpen(allowNextStateChange) if s.args.stopOnEntry { e := &dap.StoppedEvent{ Event: *newEvent("stopped"), Body: dap.StoppedEventBody{Reason: "entry", ThreadId: 1, AllThreadsStopped: true}, } s.send(e) } s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)}) if !s.args.stopOnEntry { s.runUntilStopAndNotify(api.Continue, allowNextStateChange) } } // onContinueRequest handles 'continue' request. // This is a mandatory request to support. func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) { s.send(&dap.ContinueResponse{ Response: *newResponse(request.Request), Body: dap.ContinueResponseBody{AllThreadsContinued: true}}) s.runUntilStopAndNotify(api.Continue, allowNextStateChange) } func fnName(loc *proc.Location) string { if loc.Fn == nil { return "???" } return loc.Fn.Name } func fnPackageName(loc *proc.Location) string { if loc.Fn == nil { // attribute unknown functions to the runtime return "runtime" } return loc.Fn.PackageName() } // onThreadsRequest handles 'threads' request. // This is a mandatory request to support. // It is sent in response to configurationDone response and stopped events. // Depending on the debug session stage, goroutines information // might not be available. However, the DAP spec states that // "even if a debug adapter does not support multiple threads, // it must implement the threads request and return a single // (dummy) thread". Therefore, this handler never returns // an error response. If the dummy thread is returned in its place, // the next waterfall request for its stackTrace will return the error. func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) { var err error var gs []*proc.G var next int if s.debugger != nil { gs, next, err = s.debugger.Goroutines(0, maxGoroutines) } var threads []dap.Thread if err != nil { switch err.(type) { case proc.ErrProcessExited: // If the program exits very quickly, the initial threads request will complete after it has exited. // A TerminatedEvent has already been sent. Ignore the err returned in this case. s.config.log.Debug(err) default: s.send(&dap.OutputEvent{ Event: *newEvent("output"), Body: dap.OutputEventBody{ Output: fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()), Category: "stderr", }}) } threads = []dap.Thread{{Id: 1, Name: "Dummy"}} } else if len(gs) == 0 { threads = []dap.Thread{{Id: 1, Name: "Dummy"}} } else { state, err := s.debugger.State( /*nowait*/ true) if err != nil { s.config.log.Debug("Unable to get debugger state: ", err) } if next >= 0 { s.logToConsole(fmt.Sprintf("Too many goroutines, only loaded %d", len(gs))) // Make sure the selected goroutine is included in the list of threads // to return. if state != nil && state.SelectedGoroutine != nil { var selectedFound bool for _, g := range gs { if g.ID == state.SelectedGoroutine.ID { selectedFound = true break } } if !selectedFound { g, err := s.debugger.FindGoroutine(state.SelectedGoroutine.ID) if err != nil { s.config.log.Debug("Error getting selected goroutine: ", err) } else { // TODO(suzmue): Consider putting the selected goroutine at the top. // To be consistent we may want to do this for all threads requests. gs = append(gs, g) } } } } threads = make([]dap.Thread, len(gs)) s.debugger.LockTarget() defer s.debugger.UnlockTarget() for i, g := range gs { selected := "" if state != nil && state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID { selected = "* " } thread := "" if g.Thread != nil && g.Thread.ThreadID() != 0 { thread = fmt.Sprintf(" (Thread %d)", g.Thread.ThreadID()) } // File name and line number are communicated via `stackTrace` // so no need to include them here. loc := g.UserCurrent() threads[i].Name = fmt.Sprintf("%s[Go %d] %s%s", selected, g.ID, fnName(&loc), thread) threads[i].Id = g.ID } } response := &dap.ThreadsResponse{ Response: *newResponse(request.Request), Body: dap.ThreadsResponseBody{Threads: threads}, } s.send(response) } // onAttachRequest handles 'attach' request. // This is a mandatory request to support. // Attach debug sessions support the following modes: // -- [DEFAULT] "local" -- attaches debugger to a local running process // Required args: processID // -- "remote" - attaches client to a debugger already attached to a process // Required args: none (host/port are used externally to connect) func (s *Session) onAttachRequest(request *dap.AttachRequest) { var args AttachConfig = defaultAttachConfig // narrow copy for initializing non-zero default values if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil { s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - %v", err)) return } mode := args.Mode switch mode { case "": mode = "local" fallthrough case "local": if s.debugger != nil { s.sendShowUserErrorResponse( request.Request, FailedToAttach, "Failed to attach", "debugger already started - use remote mode to connect") return } if args.ProcessID == 0 { s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", "The 'processId' attribute is missing in debug configuration") return } s.config.Debugger.AttachPid = args.ProcessID s.config.log.Debugf("attaching to pid %d", args.ProcessID) if backend := args.Backend; backend != "" { s.config.Debugger.Backend = backend } else { s.config.Debugger.Backend = "default" } 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 } 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") // TODO(polina): once we allow initialize and attach request while running, // halt before sending initialized event. onConfigurationDone will restart // execution if user requested !stopOnEntry. // 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}}}) } default: s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - unsupported 'mode' attribute %q", args.Mode)) return } if err := s.setLaunchAttachArgs(args.LaunchAttachCommonConfig); err != nil { s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", err.Error()) return } // Notify the client that the debugger is ready to start accepting // configuration requests for setting breakpoints, etc. The client // will end the configuration sequence with 'configurationDone'. s.send(&dap.InitializedEvent{Event: *newEvent("initialized")}) s.send(&dap.AttachResponse{Response: *newResponse(request.Request)}) } // onNextRequest handles 'next' request. // This is a mandatory request to support. func (s *Session) onNextRequest(request *dap.NextRequest, allowNextStateChange chan struct{}) { s.sendStepResponse(request.Arguments.ThreadId, &dap.NextResponse{Response: *newResponse(request.Request)}) s.stepUntilStopAndNotify(api.Next, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange) } // onStepInRequest handles 'stepIn' request // This is a mandatory request to support. func (s *Session) onStepInRequest(request *dap.StepInRequest, allowNextStateChange chan struct{}) { s.sendStepResponse(request.Arguments.ThreadId, &dap.StepInResponse{Response: *newResponse(request.Request)}) s.stepUntilStopAndNotify(api.Step, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange) } // onStepOutRequest handles 'stepOut' request // This is a mandatory request to support. func (s *Session) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange chan struct{}) { s.sendStepResponse(request.Arguments.ThreadId, &dap.StepOutResponse{Response: *newResponse(request.Request)}) s.stepUntilStopAndNotify(api.StepOut, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange) } func (s *Session) sendStepResponse(threadId int, message dap.Message) { // All of the threads will be continued by this request, so we need to send // a continued event so the UI can properly reflect the current state. s.send(&dap.ContinuedEvent{ Event: *newEvent("continued"), Body: dap.ContinuedEventBody{ ThreadId: threadId, AllThreadsContinued: true, }, }) s.send(message) } func stoppedGoroutineID(state *api.DebuggerState) (id int) { 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) (int, *api.Breakpoint) { // Check if the selected goroutine is stopped on a real breakpoint // since we would prefer to use that one. goid := stoppedGoroutineID(state) if g, _ := s.debugger.FindGoroutine(goid); g != nil && g.Thread != nil { if bp := g.Thread.Breakpoint(); bp != nil && bp.Breakpoint != nil && !bp.Breakpoint.Tracepoint { return goid, api.ConvertBreakpoint(bp.Breakpoint) } } // Some of the breakpoints may be log points, choose the goroutine // that is not stopped on a tracepoint. for _, th := range state.Threads { if bp := th.Breakpoint; bp != nil { if !bp.Tracepoint { return th.GoroutineID, bp } } } return 0, nil } // stepUntilStopAndNotify is a wrapper around runUntilStopAndNotify that // first switches selected goroutine. allowNextStateChange is // a channel that will be closed to signal that an // asynchornous command has completed setup or was interrupted // due to an error, so the server is ready to receive new requests. func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange chan struct{}) { defer closeIfOpen(allowNextStateChange) _, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: threadId}, nil) if err != nil { s.config.log.Errorf("Error switching goroutines while stepping: %v", err) // If we encounter an error, we will have to send a stopped event // since we already sent the step response. stopped := &dap.StoppedEvent{Event: *newEvent("stopped")} stopped.Body.AllThreadsStopped = true if state, err := s.debugger.State(false); err != nil { s.config.log.Errorf("Error retrieving state: %e", err) } else { stopped.Body.ThreadId = 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() _, 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(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(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 != "" { 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.Source.PresentationHint = "deemphasize" } stackFrames = append(stackFrames, stackFrame) } totalFrames := len(frames) if len(frames) >= start+levels && !frames[len(frames)-1].Bottom { // We don't know the exact number of available stack frames, so // add an arbitrary number so the client knows to request additional // frames. totalFrames += s.args.stackTraceDepth } response := &dap.StackTraceResponse{ Response: *newResponse(request.Request), Body: dap.StackTraceResponseBody{StackFrames: stackFrames, TotalFrames: totalFrames}, } s.send(response) } // onScopesRequest handles 'scopes' requests. // This is a mandatory request to support. // It is automatically sent as part of the threads > stacktrace > scopes > variables // "waterfall" to highlight the topmost frame at stops, after an evaluate request // for the selected scope or when a user selects different scopes in the UI. func (s *Session) onScopesRequest(request *dap.ScopesRequest) { sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId) if !ok { s.sendErrorResponse(request.Request, UnableToListLocals, "Unable to list locals", fmt.Sprintf("unknown frame id %d", request.Arguments.FrameId)) return } goid := sf.(stackFrame).goroutineID frame := sf.(stackFrame).frameIndex // Check if the function is optimized. fn, err := s.debugger.Function(goid, frame, 0, DefaultLoadConfig) if fn == nil || err != nil { s.sendErrorResponse(request.Request, UnableToListArgs, "Unable to find enclosing function", err.Error()) return } suffix := "" if fn.Optimized() { suffix = " (warning: optimized function)" } // Retrieve arguments args, err := s.debugger.FunctionArguments(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(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) } 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, err := s.childrenToDAPVariables(v) if err != nil { s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error()) return } 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 && count == 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 } func getIndexedVariableCount(c *proc.Variable) int { indexedVars := 0 switch c.Kind { case reflect.Array, reflect.Slice, reflect.Map: indexedVars = int(c.Len) } return indexedVars } // childrenToDAPVariables returns the DAP presentation of the referenced variable's children. func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) { // 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 represet key:value format. // Otherwise, we must return separate variables for both. if keyref > 0 && valref > 0 { // Both are not scalars keyvar := dap.Variable{ Name: fmt.Sprintf("[key %d]", v.startIndex+kvIndex), EvaluateName: keyexpr, Type: keyType, Value: key, VariablesReference: keyref, IndexedVariables: getIndexedVariableCount(keyv), NamedVariables: getNamedVariableCount(keyv), } valvar := dap.Variable{ Name: fmt.Sprintf("[val %d]", v.startIndex+kvIndex), EvaluateName: valexpr, Type: valType, Value: val, VariablesReference: valref, IndexedVariables: getIndexedVariableCount(valv), NamedVariables: getNamedVariableCount(valv), } children = append(children, keyvar, valvar) } else { // At least one is a scalar keyValType := valType if len(keyType) > 0 && len(valType) > 0 { keyValType = fmt.Sprintf("%s: %s", keyType, valType) } kvvar := dap.Variable{ Name: key, EvaluateName: valexpr, Type: keyValType, Value: val, } if keyref != 0 { // key is a type to be expanded if len(key) > maxMapKeyValueLen { // Truncate and make unique kvvar.Name = fmt.Sprintf("%s... @ %#x", key[0:maxMapKeyValueLen], keyv.Addr) } kvvar.VariablesReference = keyref kvvar.IndexedVariables = getIndexedVariableCount(keyv) kvvar.NamedVariables = getNamedVariableCount(keyv) } else if valref != 0 { // val is a type to be expanded kvvar.VariablesReference = valref kvvar.IndexedVariables = getIndexedVariableCount(valv) kvvar.NamedVariables = getNamedVariableCount(valv) } children = append(children, kvvar) } } case reflect.Slice, reflect.Array: children = make([]dap.Variable, len(v.Children)) for i := range v.Children { idx := v.startIndex + i cfqname := fmt.Sprintf("%s[%d]", v.fullyQualifiedNameOrExpr, idx) cvalue, cvarref := s.convertVariable(&v.Children[i], cfqname) children[i] = dap.Variable{ Name: fmt.Sprintf("[%d]", idx), EvaluateName: cfqname, Type: s.getTypeIfSupported(&v.Children[i]), Value: cvalue, VariablesReference: cvarref, IndexedVariables: getIndexedVariableCount(&v.Children[i]), NamedVariables: getNamedVariableCount(&v.Children[i]), } } default: children = make([]dap.Variable, len(v.Children)) for i := range v.Children { c := &v.Children[i] cfqname := fmt.Sprintf("%s.%s", v.fullyQualifiedNameOrExpr, c.Name) if strings.HasPrefix(c.Name, "~") || strings.HasPrefix(c.Name, ".") { cfqname = "" } else if v.isScope && v.fullyQualifiedNameOrExpr == "" { cfqname = c.Name } else if v.fullyQualifiedNameOrExpr == "" { cfqname = "" } else if v.Kind == reflect.Interface { cfqname = fmt.Sprintf("%s.(%s)", v.fullyQualifiedNameOrExpr, c.Name) // c is data } else if v.Kind == reflect.Ptr { cfqname = fmt.Sprintf("(*%v)", v.fullyQualifiedNameOrExpr) // c is the nameless pointer value } else if v.Kind == reflect.Complex64 || v.Kind == reflect.Complex128 { cfqname = "" // complex children are not struct fields and can't be accessed directly } cvalue, cvarref := s.convertVariable(c, cfqname) // Annotate any shadowed variables to "(name)" in order // to distinguish from non-shadowed variables. // TODO(suzmue): should we support a special evaluateName syntax that // can access shadowed variables? name := c.Name if c.Flags&proc.VariableShadowed == proc.VariableShadowed { name = fmt.Sprintf("(%s)", name) } children[i] = dap.Variable{ Name: name, EvaluateName: cfqname, Type: s.getTypeIfSupported(c), Value: cvalue, VariablesReference: cvarref, IndexedVariables: getIndexedVariableCount(c), NamedVariables: getNamedVariableCount(c), } } } return children, nil } func getNamedVariableCount(v *proc.Variable) int { namedVars := 0 if v.Kind == reflect.Map && v.Len > 0 { // len namedVars += 1 } if isListOfBytesOrRunes(v) { // string value of array/slice of bytes and runes. namedVars += 1 } return namedVars } // metadataToDAPVariables returns the DAP presentation of the referenced variable's metadata. // These are included as named variables func (s *Session) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) { children := []dap.Variable{} // must return empty array, not null, if no children if v.Kind == reflect.Map && v.Len > 0 { children = append(children, dap.Variable{ Name: "len()", Value: fmt.Sprintf("%d", v.Len), Type: "int", EvaluateName: fmt.Sprintf("len(%s)", v.fullyQualifiedNameOrExpr), }) } if isListOfBytesOrRunes(v.Variable) { // Return the string value of []byte or []rune. typeName := api.PrettyTypeName(v.DwarfType) loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr) s.config.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr) // We know that this is an array/slice of Uint8 or Int32, so we will load up to MaxStringLen. config := DefaultLoadConfig config.MaxArrayValues = config.MaxStringLen vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config) if err == nil { val := s.convertVariableToString(vLoaded) // TODO(suzmue): Add evaluate name. Using string(name) will not get the same result because the // MaxArrayValues is not auto adjusted in evaluate requests like MaxStringLen is adjusted. children = append(children, dap.Variable{ Name: "string()", Value: val, Type: "string", }) } else { s.config.log.Debugf("failed to load %q: %v", v.fullyQualifiedNameOrExpr, err) } } return children, nil } func isListOfBytesOrRunes(v *proc.Variable) bool { if len(v.Children) > 0 && (v.Kind == reflect.Array || v.Kind == reflect.Slice) { childKind := v.Children[0].RealType.Common().ReflectKind return childKind == reflect.Uint8 || childKind == reflect.Int32 } return false } func (s *Session) getTypeIfSupported(v *proc.Variable) string { if !s.clientCapabilities.supportsVariableType { return "" } return v.TypeString() } // convertVariable converts proc.Variable to dap.Variable value and reference // while keeping track of the full qualified name or load expression. // Variable reference is used to keep track of the children associated with each // variable. It is shared with the host via scopes or evaluate response and is an index // into the s.variableHandles map, used to look up variables and their children on // subsequent variables requests. A positive reference signals the host that another // variables request can be issued to get the elements of the compound variable. As a // custom, a zero reference, reminiscent of a zero pointer, is used to indicate that // a scalar variable cannot be "dereferenced" to get its elements (as there are none). func (s *Session) convertVariable(v *proc.Variable, qualifiedNameOrExpr string) (value string, variablesReference int) { return s.convertVariableWithOpts(v, qualifiedNameOrExpr, 0) } func (s *Session) convertVariableToString(v *proc.Variable) string { val, _ := s.convertVariableWithOpts(v, "", skipRef) return val } const ( // Limit the length of a string representation of a compound or reference type variable. maxVarValueLen = 1 << 8 // 256 // Limit the length of an inlined map key. maxMapKeyValueLen = 64 ) // Flags for convertVariableWithOpts option. type convertVariableFlags uint8 const ( skipRef convertVariableFlags = 1 << iota showFullValue ) // convertVariableWithOpts allows to skip reference generation in case all we need is // a string representation of the variable. When the variable is a compound or reference // type variable and its full string representation can be larger than defaultMaxValueLen, // this returns a truncated value unless showFull option flag is set. func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, opts convertVariableFlags) (value string, variablesReference int) { canHaveRef := false maybeCreateVariableHandle := func(v *proc.Variable) int { canHaveRef = true if opts&skipRef != 0 { return 0 } return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/, 0}) } value = api.ConvertVar(v).SinglelineString() if v.Unreadable != nil { return value, 0 } // Some of the types might be fully or partially not loaded based on LoadConfig. // Those that are fully missing (e.g. due to hitting MaxVariableRecurse), can be reloaded in place. var reloadVariable = func(v *proc.Variable, qualifiedNameOrExpr string) (value string) { // We might be loading variables from the frame that's not topmost, so use // frame-independent address-based expression, not fully-qualified name as per // https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables. // TODO(polina): Get *proc.Variable object from debugger instead. Export a function to set v.loaded to false // and call v.loadValue gain with a different load config. It's more efficient, and it's guaranteed to keep // working with generics. value = api.ConvertVar(v).SinglelineString() typeName := api.PrettyTypeName(v.DwarfType) loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr) s.config.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr) // Make sure we can load the pointers directly, not by updating just the child // This is not really necessary now because users have no way of setting FollowPointers to false. config := DefaultLoadConfig config.FollowPointers = true vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config) if err != nil { value += fmt.Sprintf(" - FAILED TO LOAD: %s", err) } else { v.Children = vLoaded.Children value = api.ConvertVar(v).SinglelineString() } return value } switch v.Kind { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: n, _ := strconv.ParseUint(api.ConvertVar(v).Value, 10, 64) value = fmt.Sprintf("%s = %#x", value, n) case reflect.UnsafePointer: // Skip child reference case reflect.Ptr: if v.DwarfType != nil && len(v.Children) > 0 && v.Children[0].Addr != 0 && v.Children[0].Kind != reflect.Invalid { if v.Children[0].OnlyAddr { // Not loaded if v.Addr == 0 { // This is equvalent to the following with the cli: // (dlv) p &a7 // (**main.FooBar)(0xc0000a3918) // // TODO(polina): what is more appropriate? // Option 1: leave it unloaded because it is a special case // Option 2: load it, but then we have to load the child, not the parent, unlike all others // TODO(polina): see if reloadVariable can be reused here cTypeName := api.PrettyTypeName(v.Children[0].DwarfType) cLoadExpr := fmt.Sprintf("*(*%q)(%#x)", cTypeName, v.Children[0].Addr) s.config.log.Debugf("loading *(%s) (type %s) with %s", qualifiedNameOrExpr, cTypeName, cLoadExpr) cLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, cLoadExpr, DefaultLoadConfig) if err != nil { value += fmt.Sprintf(" - FAILED TO LOAD: %s", err) } else { cLoaded.Name = v.Children[0].Name // otherwise, this will be the pointer expression v.Children = []proc.Variable{*cLoaded} value = api.ConvertVar(v).SinglelineString() } } else { value = reloadVariable(v, qualifiedNameOrExpr) } } if !v.Children[0].OnlyAddr { variablesReference = maybeCreateVariableHandle(v) } } case reflect.Slice, reflect.Array: if v.Len > int64(len(v.Children)) { // Not fully loaded if v.Base != 0 && len(v.Children) == 0 { // Fully missing value = reloadVariable(v, qualifiedNameOrExpr) } else if !s.clientCapabilities.supportsVariablePaging { value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children), v.Len) + value } } if v.Base != 0 && len(v.Children) > 0 { variablesReference = maybeCreateVariableHandle(v) } case reflect.Map: if v.Len > int64(len(v.Children)/2) { // Not fully loaded if len(v.Children) == 0 { // Fully missing value = reloadVariable(v, qualifiedNameOrExpr) } else if !s.clientCapabilities.supportsVariablePaging { value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children)/2, v.Len) + value } } if v.Base != 0 && len(v.Children) > 0 { variablesReference = maybeCreateVariableHandle(v) } case reflect.String: // TODO(polina): implement auto-loading here. case reflect.Interface: if v.Addr != 0 && len(v.Children) > 0 && v.Children[0].Kind != reflect.Invalid && v.Children[0].Addr != 0 { if v.Children[0].OnlyAddr { // Not loaded value = reloadVariable(v, qualifiedNameOrExpr) } if !v.Children[0].OnlyAddr { variablesReference = maybeCreateVariableHandle(v) } } case reflect.Struct: if v.Len > int64(len(v.Children)) { // Not fully loaded if len(v.Children) == 0 { // Fully missing value = reloadVariable(v, qualifiedNameOrExpr) } else { // Partially missing (TODO) value = fmt.Sprintf("(loaded %d/%d) ", len(v.Children), v.Len) + value } } if len(v.Children) > 0 { variablesReference = maybeCreateVariableHandle(v) } case reflect.Complex64, reflect.Complex128: v.Children = make([]proc.Variable, 2) v.Children[0].Name = "real" v.Children[0].Value = constant.Real(v.Value) v.Children[1].Name = "imaginary" v.Children[1].Value = constant.Imag(v.Value) if v.Kind == reflect.Complex64 { v.Children[0].Kind = reflect.Float32 v.Children[1].Kind = reflect.Float32 } else { v.Children[0].Kind = reflect.Float64 v.Children[1].Kind = reflect.Float64 } fallthrough default: // Complex, Scalar, Chan, Func if len(v.Children) > 0 { variablesReference = maybeCreateVariableHandle(v) } } // By default, only values of variables that have children can be truncated. // If showFullValue is set, then all value strings are not truncated. canTruncateValue := showFullValue&opts == 0 if len(value) > maxVarValueLen && canTruncateValue && canHaveRef { value = value[:maxVarValueLen] + "..." } return value, variablesReference } // onEvaluateRequest handles 'evalute' 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 // TODO(polina): users have complained about having to click to expand multi-level // variables, so consider also adding the following: // -- print {expression} - return the result as a string like from dlv cli func (s *Session) onEvaluateRequest(request *dap.EvaluateRequest) { showErrorToUser := request.Arguments.Context != "watch" && request.Arguments.Context != "repl" && request.Arguments.Context != "hover" if s.debugger == nil { s.sendErrorResponseWithOpts(request.Request, UnableToEvaluateExpression, "Unable to evaluate expression", "debugger is nil", showErrorToUser) return } // Default to the topmost stack frame of the current goroutine in case // no frame is specified (e.g. when stopped on entry or no call stack frame is expanded) goid, frame := -1, 0 if sf, ok := s.stackFrameHandles.get(request.Arguments.FrameId); ok { goid = sf.(stackFrame).goroutineID frame = sf.(stackFrame).frameIndex } response := &dap.EvaluateResponse{Response: *newResponse(request.Request)} isCall, err := regexp.MatchString(`^\s*call\s+\S+`, request.Arguments.Expression) if err == nil && isCall { // call {expression} expr := strings.Replace(request.Arguments.Expression, "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(goid, frame, 0, request.Arguments.Expression, 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(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, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar), NamedVariables: getNamedVariableCount(exprVar)} } s.send(response) } func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*proc.Variable, error) { // This call might be evaluated in the context of the frame that is not topmost // if the editor is set to view the variables for one of the parent frames. // If the call expression refers to any of these variables, unlike regular // expressions, it will evaluate them in the context of the topmost frame, // and the user will get an unexpected result or an unexpected symbol error. // We prevent this but disallowing any frames other than topmost. if frame > 0 { return nil, nil, fmt.Errorf("call is only supported with topmost stack frame") } stateBeforeCall, err := s.debugger.State( /*nowait*/ true) if err != nil { return nil, nil, err } // The return values of injected function calls are volatile. // Load as much useful data as possible. // TODO: investigate whether we need to increase other limits. For example, // the return value is a pointer to a temporary object, which can become // invalid by other injected function calls. Do we care about such use cases? loadCfg := DefaultLoadConfig loadCfg.MaxStringLen = maxStringLenInCallRetVars // TODO(polina): since call will resume execution of all goroutines, // we should do this asynchronously and send a continued event to the // editor, followed by a stop event when the call completes. state, err := s.debugger.Command(&api.DebuggerCommand{ Name: api.Call, ReturnInfoLoadConfig: api.LoadConfigFromProc(&loadCfg), Expr: expr, UnsafeCall: false, GoroutineID: goid, }, nil) if processExited(state, err) { 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 simultaenous 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 = stoppedGoroutineID(state) stopped.Body.Reason = s.debugger.StopReason().String() s.send(stopped) } // onTerminateRequest sends a not-yet-implemented error response. // Capability 'supportsTerminateRequest' is not set in 'initialize' response. func (s *Session) onTerminateRequest(request *dap.TerminateRequest) { s.sendNotYetImplementedErrorResponse(request.Request) } // onRestartRequest sends a not-yet-implemented error response // Capability 'supportsRestartRequest' is not set in 'initialize' response. func (s *Session) onRestartRequest(request *dap.RestartRequest) { s.sendNotYetImplementedErrorResponse(request.Request) } // onStepBackRequest handles 'stepBack' request. // This is an optional request enabled by capability ‘supportsStepBackRequest’. func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange chan struct{}) { s.sendStepResponse(request.Arguments.ThreadId, &dap.StepBackResponse{Response: *newResponse(request.Request)}) s.stepUntilStopAndNotify(api.ReverseNext, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange) } // onReverseContinueRequest performs a rewind command call up to the previous // breakpoint or the start of the process // This is an optional request enabled by capability ‘supportsStepBackRequest’. func (s *Session) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange chan struct{}) { s.send(&dap.ReverseContinueResponse{ Response: *newResponse(request.Request), }) s.runUntilStopAndNotify(api.Rewind, allowNextStateChange) } // computeEvaluateName finds the named child, and computes its evaluate name. func (s *Session) computeEvaluateName(v *fullyQualifiedVariable, cname string) (string, error) { children, err := s.childrenToDAPVariables(v) if err != nil { return "", err } 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(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 currentlly 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(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 debugee 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) } // onDisassembleRequest sends a not-yet-implemented error response. // Capability 'supportsDisassembleRequest' is not set 'initialize' response. func (s *Session) onDisassembleRequest(request *dap.DisassembleRequest) { s.sendNotYetImplementedErrorResponse(request.Request) } // 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 := 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.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) { switch bpState.Breakpoint.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 { 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 int, g *proc.G) (string, error) { frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0) if err != nil { return "", err } apiFrames, err := s.debugger.ConvertStacktrace(frames, nil) if err != nil { return "", err } var buf bytes.Buffer fmt.Fprintln(&buf, "Stack:") userLoc := g.UserCurrent() userFuncPkg := fnPackageName(&userLoc) api.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false, func(s api.Stackframe) bool { // Include all stack frames if the stack trace is for a system goroutine, // otherwise, skip runtime stack frames. if userFuncPkg == "runtime" { return true } return s.Location.Function != nil && !strings.HasPrefix(s.Location.Function.Name(), "runtime.") }) return buf.String(), nil } func (s *Session) throwReason(goroutineID int) (string, error) { return s.getExprString("s", goroutineID, 0) } func (s *Session) panicReason(goroutineID int) (string, error) { return s.getExprString("(*msgs).arg.(data)", goroutineID, 0) } func (s *Session) getExprString(expr string, goroutineID, 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.Id = id er.Body.Error.Format = fmt.Sprintf("%s: %s", summary, details) er.Body.Error.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.Id = InternalError er.Body.Error.Format = fmt.Sprintf("%s: %s", er.Message, details) s.config.log.Debug(er.Body.Error.Format) s.send(er) } func (s *Session) sendUnsupportedErrorResponse(request dap.Request) { s.sendErrorResponse(request, UnsupportedCommand, "Unsupported command", fmt.Sprintf("cannot process %q request", request.Command)) } func (s *Session) sendNotYetImplementedErrorResponse(request dap.Request) { s.sendErrorResponse(request, NotYetImplemented, "Not yet implemented", fmt.Sprintf("cannot process %q request", request.Command)) } func newResponse(request dap.Request) *dap.Response { return &dap.Response{ ProtocolMessage: dap.ProtocolMessage{ Seq: 0, Type: "response", }, Command: request.Command, RequestSeq: request.Seq, Success: true, } } func newEvent(event string) *dap.Event { return &dap.Event{ ProtocolMessage: dap.ProtocolMessage{ Seq: 0, Type: "event", }, Event: event, } } const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation] Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)` const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint. Use 'Continue' to resume the original step command.` func (s *Session) resetHandlesForStoppedEvent() { s.stackFrameHandles.reset() s.variableHandles.reset() s.exceptionErr = nil } func processExited(state *api.DebuggerState, err error) bool { _, isexited := err.(proc.ErrProcessExited) return isexited || err == nil && state.Exited } func (s *Session) setRunningCmd(running bool) { s.runningMu.Lock() defer s.runningMu.Unlock() s.runningCmd = running } func (s *Session) isRunningCmd() bool { s.runningMu.Lock() defer s.runningMu.Unlock() return s.runningCmd } func (s *Session) setHaltRequested(requested bool) { s.haltMu.Lock() defer s.haltMu.Unlock() s.haltRequested = requested } func (s *Session) checkHaltRequested() bool { s.haltMu.Lock() defer s.haltMu.Unlock() return s.haltRequested } // resumeOnce is a helper function to resume the execution // of the target when the program is halted. func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{}) (bool, *api.DebuggerState, error) { // No other goroutines should be able to try to resume // or halt execution while this goroutine is resuming // execution, so we do not miss those events. asyncSetupDone := make(chan struct{}, 1) defer closeIfOpen(asyncSetupDone) s.changeStateMu.Lock() go func() { defer s.changeStateMu.Unlock() defer closeIfOpen(allowNextStateChange) <-asyncSetupDone }() // There may have been a manual halt while the program was // stopped. If this happened, do not resume execution of // the program. if s.checkHaltRequested() { state, err := s.debugger.State(false) return false, state, err } state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone) return true, state, err } // runUntilStopAndNotify runs a debugger command until it stops on // termination, error, breakpoint, etc, when an appropriate // event needs to be sent to the client. allowNextStateChange is // a channel that will be closed to signal that an // asynchornous command has completed setup or was interrupted // due to an error, so the server is ready to receive new requests. func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange chan struct{}) { state, err := s.runUntilStop(command, allowNextStateChange) if processExited(state, err) { 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 = 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" var bp *api.Breakpoint if stopped.Body.ThreadId, bp = s.stoppedOnBreakpointGoroutineID(state); bp != nil { switch bp.Name { case proc.FatalThrow: stopped.Body.Reason = "exception" stopped.Body.Description = "fatal error" stopped.Body.Text, _ = s.throwReason(stopped.Body.ThreadId) case proc.UnrecoveredPanic: stopped.Body.Reason = "exception" stopped.Body.Description = "panic" stopped.Body.Text, _ = s.panicReason(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 = stoppedGoroutineID(state) } } // NOTE: If we happen to be responding to another request with an is-running // error while this one completes, it is possible that the error response // will arrive after this stopped event. s.send(stopped) // Send an output event with more information if next is in progress. if state != nil && state.NextInProgress { s.logToConsole("Step interrupted by a breakpoint. Use 'Continue' to resume the original step command.") } } func (s *Session) runUntilStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) { // Clear any manual stop requests that came in before we started running. s.setHaltRequested(false) s.setRunningCmd(true) defer s.setRunningCmd(false) var state *api.DebuggerState var err error for s.isRunningCmd() { state, err = resumeOnceAndCheckStop(s, command, allowNextStateChange) command = api.DirectionCongruentContinue } return state, err } // Make this a var so it can be stubbed in testing. var resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) { return s.resumeOnceAndCheckStop(command, allowNextStateChange) } func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) { resumed, state, err := s.resumeOnce(command, allowNextStateChange) // We should not try to process the log points if the program was not // resumed or there was an error. if !resumed || processExited(state, err) || state == nil || err != nil { s.setRunningCmd(false) return state, err } if s.debugger.StopReason() != proc.StopBreakpoint { s.setRunningCmd(false) } foundRealBreakpoint := s.handleLogPoints(state) if foundRealBreakpoint { 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) bool { foundRealBreakpoint := false for _, th := range state.Threads { if bp := th.Breakpoint; bp != nil { logged := s.logBreakpointMessage(bp, th.GoroutineID) if !logged { foundRealBreakpoint = true } } } return foundRealBreakpoint } func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool { if !bp.Tracepoint { return false } // TODO(suzmue): allow evaluate expressions within log points. if msg, ok := bp.UserData.(string); ok { 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 (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 }