diff --git a/_fixtures/fatalerror.go b/_fixtures/fatalerror.go new file mode 100644 index 00000000..ad1e9bfa --- /dev/null +++ b/_fixtures/fatalerror.go @@ -0,0 +1,6 @@ +package main + +func main() { + var f func() + go f() +} diff --git a/service/dap/server.go b/service/dap/server.go index 2514b3aa..cad8a880 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -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), diff --git a/service/dap/server_test.go b/service/dap/server_test.go index c615d86f..220685d0 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -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, }}) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 6b3d29c2..431ed1be 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -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 {