未验证 提交 30cdedae 编写于 作者: S Suzy Mueller 提交者: GitHub

service/dap: implement exception info (#2444)

* service/dap: implement exception info

* remove adding additional thread

* Fix tests

* add exceptionInfo tests

* update comments

* map paths to client paths

* remove launch.json

* remove change to ConvertEvalScope

* correct name of supportsExceptionInfoRequest

* Add TODO for deleting output event

* Print Stack header to buffer

* Try to move resolving exception info to onExceptionInfoRequest

* save the error and return if it is the current thread

* rename thread to g

* findgoroutine returns goroutine

* clean up findgoroutine

* log errors

* remove output event

* fix grammar
上级 1e9c5c3b
......@@ -2279,6 +2279,10 @@ func digits(n int) int {
const stacktraceTruncatedMessage = "(truncated)"
func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offsets bool) {
PrintStack(t.formatPath, out, stack, ind, offsets)
}
func PrintStack(formatPath func(string) string, out io.Writer, stack []api.Stackframe, ind string, offsets bool) {
if len(stack) == 0 {
return
}
......@@ -2301,7 +2305,7 @@ func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offs
continue
}
fmt.Fprintf(out, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
fmt.Fprintf(out, "%sat %s:%d\n", s, t.formatPath(stack[i].File), stack[i].Line)
fmt.Fprintf(out, "%sat %s:%d\n", s, formatPath(stack[i].File), stack[i].Line)
if offsets {
fmt.Fprintf(out, "%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset)
......@@ -2315,8 +2319,8 @@ func printStack(t *Term, out io.Writer, stack []api.Stackframe, ind string, offs
continue
}
fmt.Fprintf(out, "%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
fmt.Fprintf(out, "%sat %s:%d\n", s2, t.formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), t.formatPath(d.DeferLoc.File), d.DeferLoc.Line)
fmt.Fprintf(out, "%sat %s:%d\n", s2, formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), formatPath(d.DeferLoc.File), d.DeferLoc.Line)
}
for j := range stack[i].Arguments {
......
......@@ -100,6 +100,7 @@ func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.Init
SupportsConditionalBreakpoints: true,
SupportsDelayedStackTraceLoading: true,
SupportTerminateDebuggee: true,
SupportsExceptionInfoRequest: true,
}
if !reflect.DeepEqual(initResp.Body, wantCapabilities) {
t.Errorf("capabilities in initializeResponse: got %+v, want %v", pretty(initResp.Body), pretty(wantCapabilities))
......@@ -399,8 +400,10 @@ func (c *Client) CompletionsRequest() {
}
// ExceptionInfoRequest sends a 'exceptionInfo' request.
func (c *Client) ExceptionInfoRequest() {
c.send(&dap.ExceptionInfoRequest{Request: *c.newRequest("exceptionInfo")})
func (c *Client) ExceptionInfoRequest(threadID int) {
request := &dap.ExceptionInfoRequest{Request: *c.newRequest("exceptionInfo")}
request.Arguments.ThreadId = threadID
c.send(request)
}
// LoadedSourcesRequest sends a 'loadedSources' request.
......
......@@ -21,7 +21,8 @@ const (
UnableToListGlobals = 2007
UnableToLookupVariable = 2008
UnableToEvaluateExpression = 2009
UnableToGetExceptionInfo = 2010
// Add more codes as we support more requests
DebuggeeIsRunning = 4000
DisconnectError = 5000
)
......@@ -10,6 +10,7 @@ package dap
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"go/constant"
......@@ -29,6 +30,7 @@ import (
"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/pkg/terminal"
"github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/debugger"
......@@ -102,8 +104,13 @@ type Server struct {
variableHandles *variablesHandlesMap
// args tracks special settings for handling debug session requests.
args launchAttachArgs
<<<<<<< HEAD
// exceptionErr tracks the runtime error that last occurred.
exceptionErr error
=======
// clientCapabilities tracks special settings for handling debug session requests.
clientCapabilities dapClientCapabilites
>>>>>>> 1e9c5c3b07dc5f0f2b3b1fb17bde6444cbf7ca30
// mu synchronizes access to objects set on start-up (from run goroutine)
// and stopped on teardown (from main goroutine)
......@@ -188,6 +195,7 @@ func NewServer(config *service.Config) *Server {
stackFrameHandles: newHandlesMap(),
variableHandles: newVariablesHandlesMap(),
args: defaultArgs,
exceptionErr: nil,
}
}
......@@ -571,6 +579,9 @@ func (s *Server) handleRequest(request dap.Message) {
// 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’)
......@@ -595,10 +606,6 @@ func (s *Server) handleRequest(request dap.Message) {
case *dap.CompletionsRequest:
// Optional (capability ‘supportsCompletionsRequest’)
s.sendUnsupportedErrorResponse(request.Request)
case *dap.ExceptionInfoRequest:
// Optional (capability ‘supportsExceptionInfoRequest’)
// TODO: does this request make sense for delve?
s.sendUnsupportedErrorResponse(request.Request)
case *dap.DataBreakpointInfoRequest:
// Optional (capability ‘supportsDataBreakpoints’)
s.sendUnsupportedErrorResponse(request.Request)
......@@ -664,6 +671,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
response.Body.SupportsConditionalBreakpoints = true
response.Body.SupportsDelayedStackTraceLoading = true
response.Body.SupportTerminateDebuggee = true
response.Body.SupportsExceptionInfoRequest = true
// TODO(polina): support this to match vscode-go functionality
response.Body.SupportsSetVariable = false
// TODO(polina): support these requests in addition to vscode-go feature parity
......@@ -1161,6 +1169,7 @@ func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
threads[i].Id = g.ID
}
}
response := &dap.ThreadsResponse{
Response: *newResponse(request.Request),
Body: dap.ThreadsResponseBody{Threads: threads},
......@@ -1913,6 +1922,81 @@ func (s *Server) onCancelRequest(request *dap.CancelRequest) {
s.sendNotYetImplementedErrorResponse(request.Request)
}
// onExceptionInfoRequest handles 'exceptionInfo' requests.
// Capability 'supportsExceptionInfoRequest' is set in 'initialize' response.
func (s *Server) 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.
if bpState != nil && bpState.Breakpoint != nil && (bpState.Breakpoint.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) {
switch bpState.Breakpoint.Name {
case proc.FatalThrow:
// TODO(suzmue): add the fatal throw reason to body.Description.
body.ExceptionId = "fatal error"
case proc.UnrecoveredPanic:
body.ExceptionId = "panic"
// Attempt to get the value of the panic message.
exprVar, err := s.debugger.EvalVariableInScope(goroutineID, 0, 0, "(*msgs).arg.(data)", DefaultLoadConfig)
if err == nil {
body.Description = exprVar.Value.String()
}
}
} 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 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
}
}
frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
if err == nil && len(frames) > 0 {
apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
if err == nil {
var buf bytes.Buffer
fmt.Fprintln(&buf, "Stack:")
terminal.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false)
body.Details.StackTrace = buf.String()
}
}
response := &dap.ExceptionInfoResponse{
Response: *newResponse(request.Request),
Body: body,
}
s.send(response)
}
// sendErrorResponseWithOpts offers configuration options.
// showUser - if true, the error will be shown to the user (e.g. via a visible pop-up)
func (s *Server) sendErrorResponseWithOpts(request dap.Request, id int, summary, details string, showUser bool) {
......@@ -1987,6 +2071,7 @@ Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https
func (s *Server) resetHandlesForStoppedEvent() {
s.stackFrameHandles.reset()
s.variableHandles.reset()
s.exceptionErr = nil
}
// doRunCommand runs a debugger command until it stops on
......@@ -2014,6 +2099,9 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
stopped.Body.AllThreadsStopped = true
if err == nil {
// TODO(suzmue): If stopped.Body.ThreadId is not a valid goroutine
// then the stopped reason does not show up anywhere in the
// vscode ui.
stopped.Body.ThreadId = stoppedGoroutineID(state)
switch stopReason {
......@@ -2031,14 +2119,18 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
if state.CurrentThread != nil && state.CurrentThread.Breakpoint != nil {
switch state.CurrentThread.Breakpoint.Name {
case proc.FatalThrow:
stopped.Body.Reason = "fatal error"
stopped.Body.Reason = "exception"
stopped.Body.Description = "Paused on fatal error"
case proc.UnrecoveredPanic:
stopped.Body.Reason = "panic"
stopped.Body.Reason = "exception"
stopped.Body.Description = "Paused on panic"
}
}
} else {
s.exceptionErr = err
s.log.Error("runtime error: ", err)
stopped.Body.Reason = "runtime error"
stopped.Body.Reason = "exception"
stopped.Body.Description = "Paused on 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" {
......@@ -2048,19 +2140,6 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
if err == nil {
stopped.Body.ThreadId = stoppedGoroutineID(state)
}
// TODO(polina): according to the spec, the extra 'text' is supposed to show up in the UI (e.g. on hover),
// but so far I am unable to get this to work in vscode - see https://github.com/microsoft/vscode/issues/104475.
// Options to explore:
// - supporting ExceptionInfo request
// - virtual variable scope for Exception that shows the message (details here: https://github.com/microsoft/vscode/issues/3101)
// In the meantime, provide the extra details by outputing an error message.
s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: fmt.Sprintf("ERROR: %s\n", stopped.Body.Text),
Category: "stderr",
}})
}
// NOTE: If we happen to be responding to another request with an is-running
......
......@@ -2698,14 +2698,16 @@ func TestBadAccess(t *testing.T) {
expectStoppedOnError := func(errorPrefix string) {
t.Helper()
oe := client.ExpectOutputEvent(t)
if oe.Body.Category != "stderr" || !strings.HasPrefix(oe.Body.Output, "ERROR: "+errorPrefix) {
t.Errorf("\ngot %#v\nwant Category=\"stderr\" Output=\"%s ...\"", oe, errorPrefix)
}
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "runtime error" || !strings.HasPrefix(se.Body.Text, errorPrefix) {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"runtime error\" Text=\"%s\"", se, errorPrefix)
if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on runtime error" || !strings.HasPrefix(se.Body.Text, errorPrefix) {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on runtime error\" Text=\"%s\"", se, errorPrefix)
}
client.ExceptionInfoRequest(1)
eInfo := client.ExpectExceptionInfoResponse(t)
if eInfo.Body.ExceptionId != "runtime error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) {
t.Errorf("\ngot %#v\nwant ExceptionId=\"runtime error\" Text=\"%s\"", eInfo, errorPrefix)
}
}
client.ContinueRequest(1)
......@@ -2750,8 +2752,14 @@ func TestPanicBreakpointOnContinue(t *testing.T) {
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"panic\"", se)
if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on panic" {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on panic\"", se)
}
client.ExceptionInfoRequest(1)
eInfo := client.ExpectExceptionInfoResponse(t)
if eInfo.Body.ExceptionId != "panic" || eInfo.Body.Description != "\"BOOM!\"" {
t.Errorf("\ngot %#v\nwant ExceptionId=\"panic\" Description=\"\"BOOM!\"\"", eInfo)
}
},
disconnect: true,
......@@ -2782,9 +2790,14 @@ func TestPanicBreakpointOnNext(t *testing.T) {
client.ExpectNextResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "Paused on panic" {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"Paused on panic\"", se)
}
if se.Body.ThreadId != 1 || se.Body.Reason != "panic" {
t.Errorf("\ngot %#v\nexpected ThreadId=1 Reason=\"panic\"", se)
client.ExceptionInfoRequest(1)
eInfo := client.ExpectExceptionInfoResponse(t)
if eInfo.Body.ExceptionId != "panic" || eInfo.Body.Description != "\"BOOM!\"" {
t.Errorf("\ngot %#v\nwant ExceptionId=\"panic\" Description=\"\"BOOM!\"\"", eInfo)
}
},
disconnect: true,
......@@ -2809,8 +2822,8 @@ func TestFatalThrowBreakpoint(t *testing.T) {
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.Reason != "fatal error" {
t.Errorf("\ngot %#v\nwant Reason=\"fatal error\"", se)
if se.Body.Reason != "exception" || se.Body.Description != "Paused on fatal error" {
t.Errorf("\ngot %#v\nwant Reason=\"exception\" Description=\"Paused on fatal error\"", se)
}
},
disconnect: true,
......@@ -3171,9 +3184,6 @@ func TestUnupportedCommandResponses(t *testing.T) {
client.CompletionsRequest()
expectUnsupportedCommand("completions")
client.ExceptionInfoRequest()
expectUnsupportedCommand("exceptionInfo")
client.DataBreakpointInfoRequest()
expectUnsupportedCommand("dataBreakpointInfo")
......
......@@ -967,6 +967,14 @@ func (d *Debugger) FindThread(id int) (proc.Thread, error) {
return nil, nil
}
// FindGoroutine returns the goroutine for the given 'id'.
func (d *Debugger) FindGoroutine(id int) (*proc.G, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return proc.FindGoroutine(d.target, id)
}
func (d *Debugger) setRunning(running bool) {
d.runningMutex.Lock()
d.running = running
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册