service/dap: add throw reason to exception info (#2524)

We can get the throw reason by looking at the argument "s" in runtime.throw. This is not currently working in Go 1.16 or Go 1.17 (see golang/go#46425), but does work in Go 1.15 and Go 1.14
This commit is contained in:
Suzy Mueller 2021-06-28 11:39:34 -04:00 committed by GitHub
parent 7411290468
commit 1ebfc5c37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 3 deletions

6
_fixtures/fatalerror.go Normal file

@ -0,0 +1,6 @@
package main
func main() {
var f func()
go f()
}

@ -29,6 +29,7 @@ import (
"sync" "sync"
"github.com/go-delve/delve/pkg/gobuild" "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/locspec"
"github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc"
@ -2509,14 +2510,36 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
if bpState != nil && bpState.Breakpoint != nil && (bpState.Breakpoint.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) { if bpState != nil && bpState.Breakpoint != nil && (bpState.Breakpoint.Name == proc.FatalThrow || bpState.Breakpoint.Name == proc.UnrecoveredPanic) {
switch bpState.Breakpoint.Name { switch bpState.Breakpoint.Name {
case proc.FatalThrow: case proc.FatalThrow:
// TODO(suzmue): add the fatal throw reason to body.Description.
body.ExceptionId = "fatal error" body.ExceptionId = "fatal error"
// Attempt to get the value of the throw reason.
// This is not currently working for Go 1.16 or 1.17: https://github.com/golang/go/issues/46425.
handleError := func(err error) {
if err != nil {
body.Description = fmt.Sprintf("Error getting throw reason: %s", err.Error())
}
if goversion.ProducerAfterOrEqual(s.debugger.TargetGoVersion(), 1, 16) {
body.Description = "Throw reason unavailable, see https://github.com/golang/go/issues/46425"
}
}
exprVar, err := s.debugger.EvalVariableInScope(goroutineID, 1, 0, "s", DefaultLoadConfig)
if err == nil {
if exprVar.Value != nil {
body.Description = exprVar.Value.String()
} else {
handleError(exprVar.Unreadable)
}
} else {
handleError(err)
}
case proc.UnrecoveredPanic: case proc.UnrecoveredPanic:
body.ExceptionId = "panic" body.ExceptionId = "panic"
// Attempt to get the value of the panic message. // Attempt to get the value of the panic message.
exprVar, err := s.debugger.EvalVariableInScope(goroutineID, 0, 0, "(*msgs).arg.(data)", DefaultLoadConfig) exprVar, err := s.debugger.EvalVariableInScope(goroutineID, 0, 0, "(*msgs).arg.(data)", DefaultLoadConfig)
if err == nil { if err == nil {
body.Description = exprVar.Value.String() body.Description = exprVar.Value.String()
} else {
body.Description = fmt.Sprintf("Error getting panic message: %s", err.Error())
} }
} }
} else { } else {
@ -2545,7 +2568,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
} }
frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0) frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
if err == nil && len(frames) > 0 { if err == nil {
apiFrames, err := s.debugger.ConvertStacktrace(frames, nil) apiFrames, err := s.debugger.ConvertStacktrace(frames, nil)
if err == nil { if err == nil {
var buf bytes.Buffer var buf bytes.Buffer
@ -2562,6 +2585,8 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
}) })
body.Details.StackTrace = buf.String() body.Details.StackTrace = buf.String()
} }
} else {
body.Details.StackTrace = fmt.Sprintf("Error getting stack trace: %s", err.Error())
} }
response := &dap.ExceptionInfoResponse{ response := &dap.ExceptionInfoResponse{
Response: *newResponse(request.Request), Response: *newResponse(request.Request),

@ -3490,7 +3490,6 @@ func TestBadAccess(t *testing.T) {
if eInfo.Body.ExceptionId != "runtime error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) { if eInfo.Body.ExceptionId != "runtime error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) {
t.Errorf("\ngot %#v\nwant ExceptionId=\"runtime error\" Text=\"%s\"", eInfo, errorPrefix) t.Errorf("\ngot %#v\nwant ExceptionId=\"runtime error\" Text=\"%s\"", eInfo, errorPrefix)
} }
} }
client.ContinueRequest(1) client.ContinueRequest(1)
@ -3602,6 +3601,41 @@ func TestPanicBreakpointOnNext(t *testing.T) {
} }
func TestFatalThrowBreakpoint(t *testing.T) { func TestFatalThrowBreakpoint(t *testing.T) {
runTest(t, "fatalerror", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{3},
[]onBreakpoint{{
execute: func() {
checkStop(t, client, 1, "main.main", 3)
client.ContinueRequest(1)
client.ExpectContinueResponse(t)
se := client.ExpectStoppedEvent(t)
if se.Body.ThreadId != 1 || se.Body.Reason != "exception" || se.Body.Description != "fatal error" {
t.Errorf("\ngot %#v\nwant ThreadId=1 Reason=\"exception\" Description=\"fatal error\"", se)
}
// TODO(suzmue): Enable this test for 1.17 when https://github.com/golang/go/issues/46425 is fixed.
errorPrefix := "\"go of nil func value\""
if goversion.VersionAfterOrEqual(runtime.Version(), 1, 16) {
errorPrefix = "Throw reason unavailable, see https://github.com/golang/go/issues/46425"
}
client.ExceptionInfoRequest(1)
eInfo := client.ExpectExceptionInfoResponse(t)
if eInfo.Body.ExceptionId != "fatal error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) {
t.Errorf("\ngot %#v\nwant ExceptionId=\"runtime error\" Text=%s", eInfo, errorPrefix)
}
},
disconnect: true,
}})
})
runTest(t, "testdeadlock", func(client *daptest.Client, fixture protest.Fixture) { runTest(t, "testdeadlock", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch", runDebugSessionWithBPs(t, client, "launch",
// Launch // Launch
@ -3621,6 +3655,10 @@ func TestFatalThrowBreakpoint(t *testing.T) {
if se.Body.Reason != "exception" || se.Body.Description != "fatal error" { if se.Body.Reason != "exception" || se.Body.Description != "fatal error" {
t.Errorf("\ngot %#v\nwant Reason=\"exception\" Description=\"fatal error\"", se) t.Errorf("\ngot %#v\nwant Reason=\"exception\" Description=\"fatal error\"", se)
} }
// TODO(suzmue): Get the exception info for the thread and check the description
// includes "all goroutines are asleep - deadlock!".
// Stopped events with no selected goroutines need to be supported to test deadlock.
}, },
disconnect: true, disconnect: true,
}}) }})

@ -237,6 +237,12 @@ func (d *Debugger) checkGoVersion() error {
return goversion.Compatible(producer) return goversion.Compatible(producer)
} }
func (d *Debugger) TargetGoVersion() string {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.target.BinInfo().Producer()
}
// Launch will start a process with the given args and working directory. // Launch will start a process with the given args and working directory.
func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) { func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) {
if err := verifyBinaryFormat(processArgs[0]); err != nil { if err := verifyBinaryFormat(processArgs[0]); err != nil {