Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions _fixtures/fatalerror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package main

func main() {
var f func()
go f()
}
29 changes: 27 additions & 2 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2361,14 +2362,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.
polinasok marked this conversation as resolved.
Show resolved Hide resolved
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 {
suzmue marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -2397,14 +2420,16 @@ 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
fmt.Fprintln(&buf, "Stack:")
terminal.PrintStack(s.toClientPath, &buf, apiFrames, "\t", false)
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),
Expand Down
40 changes: 39 additions & 1 deletion service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3221,7 +3221,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)
Expand Down Expand Up @@ -3320,6 +3319,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
Expand All @@ -3339,6 +3373,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!".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to test this, just gate the assertion on the Go compiler being version 1.14 or 1.15 (which we still test via CI).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual issue that is breaking this test is that the deadlock is not associated with a goroutine ID, so we can't request the exception info. This needs to be fixed too, but makes sense to do separately because we will likely want to add the thread that is running to display the stack trace etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about _fixtures/fatalerror.go? Does that one result in a selected goroutine, so you have an id linked to a description that you can check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see that fixture. I could add a fixture to do a concurrent map read / write and I could test it that way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh! That's because it was in my local client. I was going to add it at some point, but never did. This is easier than concurrent map operations:

func main() {
    var f func()
    go f()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the fixture and the test

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry again for pointing to something that only I had :) Thank you for adding the test.

About the deadlock case with no thread id. I filed an issue against vscode a while back for displaying stopped reason even with no thread id: microsoft/vscode#124532. They seem to be taking it seriously. Can we think of a related way to associate exception info with a stop that has no thread id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception info is shown in the source code, so if its not associated with a thread with a stack trace, I think there would need to be a separate UI for it.

// Stopped events with no selected goroutines need to be supported to test deadlock.
},
disconnect: true,
}})
Expand Down
6 changes: 6 additions & 0 deletions service/debugger/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down