service/dap: handle unexpected debugger termination (EOF) error (#2574)
Using issue419.go, I observed that the continue command fails with an error when debugger receives and forwards an interrupt. In spite of the stopped event, vscode still shows the state as RUNNING because the threads request is unable to retrieve any threads, but at least one dummy thread is always expected. Co-authored-by: Polina Sokolova <polinasok@users.noreply.github.com>
This commit is contained in:
parent
b41e47a305
commit
b7d8edcdaf
@ -772,11 +772,9 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if mode == "replay" {
|
if mode == "replay" {
|
||||||
traceDirPath, _ := request.Arguments["traceDirPath"].(string)
|
traceDirPath, _ := request.Arguments["traceDirPath"].(string)
|
||||||
|
|
||||||
|
|
||||||
// Validate trace directory
|
// Validate trace directory
|
||||||
if traceDirPath == "" {
|
if traceDirPath == "" {
|
||||||
s.sendErrorResponse(request.Request,
|
s.sendErrorResponse(request.Request,
|
||||||
@ -785,10 +783,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Assign the rr trace directory path to debugger configuration
|
// Assign the rr trace directory path to debugger configuration
|
||||||
s.config.Debugger.CoreFile = traceDirPath
|
s.config.Debugger.CoreFile = traceDirPath
|
||||||
s.config.Debugger.Backend = "rr"
|
s.config.Debugger.Backend = "rr"
|
||||||
@ -797,7 +791,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
if mode == "core" {
|
if mode == "core" {
|
||||||
coreFilePath, _ := request.Arguments["coreFilePath"].(string)
|
coreFilePath, _ := request.Arguments["coreFilePath"].(string)
|
||||||
|
|
||||||
|
|
||||||
// Validate core dump path
|
// Validate core dump path
|
||||||
if coreFilePath == "" {
|
if coreFilePath == "" {
|
||||||
|
|
||||||
@ -807,7 +800,6 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Assign the non-empty core file path to debugger configuration. This will
|
// Assign the non-empty core file path to debugger configuration. This will
|
||||||
// trigger a native core file replay instead of an rr trace replay
|
// trigger a native core file replay instead of an rr trace replay
|
||||||
s.config.Debugger.CoreFile = coreFilePath
|
s.config.Debugger.CoreFile = coreFilePath
|
||||||
@ -815,7 +807,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
|
s.log.Debugf("debug backend is '%s'", s.config.Debugger.Backend)
|
||||||
|
|
||||||
// Prepare the debug executable filename, build flags and build it
|
// Prepare the debug executable filename, build flags and build it
|
||||||
if mode == "debug" || mode == "test" {
|
if mode == "debug" || mode == "test" {
|
||||||
output, ok := request.Arguments["output"].(string)
|
output, ok := request.Arguments["output"].(string)
|
||||||
@ -970,7 +962,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
}
|
}
|
||||||
// Enable StepBack controls on supported backends
|
// Enable StepBack controls on supported backends
|
||||||
if s.config.Debugger.Backend == "rr" {
|
if s.config.Debugger.Backend == "rr" {
|
||||||
s.send(&dap.CapabilitiesEvent{ Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{ SupportsStepBack: true }}})
|
s.send(&dap.CapabilitiesEvent{Event: *newEvent("capabilities"), Body: dap.CapabilitiesEventBody{Capabilities: dap.Capabilities{SupportsStepBack: true}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the client that the debugger is ready to start accepting
|
// Notify the client that the debugger is ready to start accepting
|
||||||
@ -1126,7 +1118,7 @@ func (s *Server) stopDebugSession(killProcess bool) error {
|
|||||||
s.logToConsole(exited.Error())
|
s.logToConsole(exited.Error())
|
||||||
err = nil
|
err = nil
|
||||||
default:
|
default:
|
||||||
s.log.Error(err)
|
s.log.Error("detach returned error: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -1452,49 +1444,53 @@ func fnPackageName(loc *proc.Location) string {
|
|||||||
// onThreadsRequest handles 'threads' request.
|
// onThreadsRequest handles 'threads' request.
|
||||||
// This is a mandatory request to support.
|
// This is a mandatory request to support.
|
||||||
// It is sent in response to configurationDone response and stopped events.
|
// It is sent in response to configurationDone response and stopped events.
|
||||||
|
// Depending on the debug session stage, goroutines information
|
||||||
|
// might not be available. However, the DAP spec states that
|
||||||
|
// "even if a debug adapter does not support multiple threads,
|
||||||
|
// it must implement the threads request and return a single
|
||||||
|
// (dummy) thread". Therefore, this handler never returns
|
||||||
|
// an error response. If the dummy thread is returned in its place,
|
||||||
|
// the next waterfall request for its stackTrace will return the error.
|
||||||
func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
|
func (s *Server) onThreadsRequest(request *dap.ThreadsRequest) {
|
||||||
if s.debugger == nil {
|
var err error
|
||||||
s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", "debugger is nil")
|
var gs []*proc.G
|
||||||
return
|
var next int
|
||||||
|
if s.debugger != nil {
|
||||||
|
gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
|
||||||
}
|
}
|
||||||
|
threads := make([]dap.Thread, len(gs))
|
||||||
|
|
||||||
gs, next, err := s.debugger.Goroutines(0, maxGoroutines)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case proc.ErrProcessExited:
|
case proc.ErrProcessExited:
|
||||||
// If the program exits very quickly, the initial threads request will complete after it has exited.
|
// If the program exits very quickly, the initial threads request will complete after it has exited.
|
||||||
// A TerminatedEvent has already been sent. Ignore the err returned in this case.
|
// A TerminatedEvent has already been sent. Ignore the err returned in this case.
|
||||||
s.send(&dap.ThreadsResponse{Response: *newResponse(request.Request)})
|
s.log.Debug(err)
|
||||||
default:
|
default:
|
||||||
s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", err.Error())
|
s.send(&dap.OutputEvent{
|
||||||
|
Event: *newEvent("output"),
|
||||||
|
Body: dap.OutputEventBody{
|
||||||
|
Output: fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()),
|
||||||
|
Category: "stderr",
|
||||||
|
}})
|
||||||
}
|
}
|
||||||
return
|
threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
|
||||||
}
|
} else if len(threads) == 0 {
|
||||||
|
|
||||||
if next >= 0 {
|
|
||||||
s.logToConsole(fmt.Sprintf("too many goroutines, only loaded %d", len(gs)))
|
|
||||||
}
|
|
||||||
|
|
||||||
threads := make([]dap.Thread, len(gs))
|
|
||||||
if len(threads) == 0 {
|
|
||||||
// Depending on the debug session stage, goroutines information
|
|
||||||
// might not be available. However, the DAP spec states that
|
|
||||||
// "even if a debug adapter does not support multiple threads,
|
|
||||||
// it must implement the threads request and return a single
|
|
||||||
// (dummy) thread".
|
|
||||||
threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
|
threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
|
||||||
} else {
|
} else {
|
||||||
|
if next >= 0 {
|
||||||
|
s.logToConsole(fmt.Sprintf("too many goroutines, only loaded %d", len(gs)))
|
||||||
|
}
|
||||||
state, err := s.debugger.State( /*nowait*/ true)
|
state, err := s.debugger.State( /*nowait*/ true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.sendErrorResponse(request.Request, UnableToDisplayThreads, "Unable to display threads", err.Error())
|
s.log.Debug("Unable to get debugger state: ", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
s.debugger.LockTarget()
|
s.debugger.LockTarget()
|
||||||
defer s.debugger.UnlockTarget()
|
defer s.debugger.UnlockTarget()
|
||||||
|
|
||||||
for i, g := range gs {
|
for i, g := range gs {
|
||||||
selected := ""
|
selected := ""
|
||||||
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
|
if state != nil && state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
|
||||||
selected = "* "
|
selected = "* "
|
||||||
}
|
}
|
||||||
thread := ""
|
thread := ""
|
||||||
@ -1674,6 +1670,11 @@ type stackFrame struct {
|
|||||||
// As per DAP spec, this request only gets triggered as a follow-up
|
// As per DAP spec, this request only gets triggered as a follow-up
|
||||||
// to a successful threads request as part of the "request waterfall".
|
// to a successful threads request as part of the "request waterfall".
|
||||||
func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
|
func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) {
|
||||||
|
if s.debugger == nil {
|
||||||
|
s.sendErrorResponse(request.Request, UnableToProduceStackTrace, "Unable to produce stack trace", "debugger is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
goroutineID := request.Arguments.ThreadId
|
goroutineID := request.Arguments.ThreadId
|
||||||
frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
|
frames, err := s.debugger.Stacktrace(goroutineID, s.args.stackTraceDepth, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2392,7 +2393,7 @@ func (s *Server) doCall(goid, frame int, expr string) (*api.DebuggerState, []*pr
|
|||||||
UnsafeCall: false,
|
UnsafeCall: false,
|
||||||
GoroutineID: goid,
|
GoroutineID: goid,
|
||||||
}, nil)
|
}, nil)
|
||||||
if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
|
if processExited(state, err) {
|
||||||
e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
|
e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
|
||||||
s.send(e)
|
s.send(e)
|
||||||
return nil, nil, errors.New("terminated")
|
return nil, nil, errors.New("terminated")
|
||||||
@ -2825,6 +2826,11 @@ func (s *Server) resetHandlesForStoppedEvent() {
|
|||||||
s.exceptionErr = nil
|
s.exceptionErr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processExited(state *api.DebuggerState, err error) bool {
|
||||||
|
_, isexited := err.(proc.ErrProcessExited)
|
||||||
|
return isexited || err == nil && state.Exited
|
||||||
|
}
|
||||||
|
|
||||||
// doRunCommand runs a debugger command until it stops on
|
// doRunCommand runs a debugger command until it stops on
|
||||||
// termination, error, breakpoint, etc, when an appropriate
|
// termination, error, breakpoint, etc, when an appropriate
|
||||||
// event needs to be sent to the client. asyncSetupDone is
|
// event needs to be sent to the client. asyncSetupDone is
|
||||||
@ -2837,7 +2843,7 @@ func (s *Server) doRunCommand(command string, asyncSetupDone chan struct{}) {
|
|||||||
// So we should always close it ourselves just in case.
|
// So we should always close it ourselves just in case.
|
||||||
defer s.asyncCommandDone(asyncSetupDone)
|
defer s.asyncCommandDone(asyncSetupDone)
|
||||||
state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone)
|
state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone)
|
||||||
if _, isexited := err.(proc.ErrProcessExited); isexited || err == nil && state.Exited {
|
if processExited(state, err) {
|
||||||
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
|
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2929,4 +2935,4 @@ func (s *Server) toServerPath(path string) string {
|
|||||||
s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
|
s.log.Debugf("client path=%s converted to server path=%s\n", path, serverPath)
|
||||||
}
|
}
|
||||||
return serverPath
|
return serverPath
|
||||||
}
|
}
|
||||||
|
@ -438,8 +438,11 @@ func TestContinueOnEntry(t *testing.T) {
|
|||||||
// 7 >> threads, << threads
|
// 7 >> threads, << threads
|
||||||
client.ThreadsRequest()
|
client.ThreadsRequest()
|
||||||
tResp := client.ExpectThreadsResponse(t)
|
tResp := client.ExpectThreadsResponse(t)
|
||||||
if tResp.Seq != 0 || tResp.RequestSeq != 7 || len(tResp.Body.Threads) != 0 {
|
if tResp.Seq != 0 || tResp.RequestSeq != 7 || len(tResp.Body.Threads) != 1 {
|
||||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7 len(Threads)=0", tResp)
|
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7 len(Threads)=1", tResp)
|
||||||
|
}
|
||||||
|
if tResp.Body.Threads[0].Id != 1 || tResp.Body.Threads[0].Name != "Dummy" {
|
||||||
|
t.Errorf("\ngot %#v\nwant Id=1, Name=\"Dummy\"", tResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8 >> disconnect, << disconnect
|
// 8 >> disconnect, << disconnect
|
||||||
|
Loading…
Reference in New Issue
Block a user