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"
"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"
@ -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) {
switch bpState.Breakpoint.Name {
case proc.FatalThrow:
// TODO(suzmue): add the fatal throw reason to body.Description.
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:
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 {
body.Description = fmt.Sprintf("Error getting panic message: %s", err.Error())
}
}
} else {
@ -2545,7 +2568,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
}
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)
if err == nil {
var buf bytes.Buffer
@ -2562,6 +2585,8 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
})
body.Details.StackTrace = buf.String()
}
} else {
body.Details.StackTrace = fmt.Sprintf("Error getting stack trace: %s", err.Error())
}
response := &dap.ExceptionInfoResponse{
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) {
t.Errorf("\ngot %#v\nwant ExceptionId=\"runtime error\" Text=\"%s\"", eInfo, errorPrefix)
}
}
client.ContinueRequest(1)
@ -3602,6 +3601,41 @@ func TestPanicBreakpointOnNext(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) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
@ -3621,6 +3655,10 @@ func TestFatalThrowBreakpoint(t *testing.T) {
if se.Body.Reason != "exception" || se.Body.Description != "fatal error" {
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,
}})

@ -237,6 +237,12 @@ func (d *Debugger) checkGoVersion() error {
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.
func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) {
if err := verifyBinaryFormat(processArgs[0]); err != nil {