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: support pause request #2466

Merged
merged 7 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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: 3 additions & 3 deletions service/dap/daptest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@ func (c *Client) StepOutRequest(thread int) {
}

// PauseRequest sends a 'pause' request.
func (c *Client) PauseRequest() {
request := &dap.NextRequest{Request: *c.newRequest("pause")}
// TODO(polina): arguments
func (c *Client) PauseRequest(threadId int) {
request := &dap.PauseRequest{Request: *c.newRequest("pause")}
request.Arguments.ThreadId = threadId
c.send(request)
}

Expand Down
1 change: 1 addition & 0 deletions service/dap/error_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
UnableToListGlobals = 2007
UnableToLookupVariable = 2008
UnableToEvaluateExpression = 2009
UnableToHalt = 2010

DebuggeeIsRunning = 4000
DisconnectError = 5000
Expand Down
29 changes: 21 additions & 8 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,15 +360,14 @@ func (s *Server) handleRequest(request dap.Message) {
return
}

// These requests, can be handeled regardless of whether the targret is running
// These requests, can be handled regardless of whether the targret is running
switch request := request.(type) {
case *dap.DisconnectRequest:
// Required
s.onDisconnectRequest(request)
return
case *dap.PauseRequest:
// Required
// TODO: implement this request in V0
s.onPauseRequest(request)
return
case *dap.TerminateRequest:
Expand Down Expand Up @@ -1220,10 +1219,20 @@ func (s *Server) doStepCommand(command string, threadId int, asyncSetupDone chan
s.doCommand(command, asyncSetupDone)
}

// onPauseRequest sends a not-yet-implemented error response.
// onPauseRequest handles 'pause' request.
// This is a mandatory request to support.
func (s *Server) onPauseRequest(request *dap.PauseRequest) { // TODO V0
s.sendNotYetImplementedErrorResponse(request.Request)
func (s *Server) onPauseRequest(request *dap.PauseRequest) {
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
if err != nil {
s.sendErrorResponse(request.Request, UnableToHalt, "Unable to halt execution", err.Error())
return
}
s.send(&dap.PauseResponse{Response: *newResponse(request.Request)})
// No need to send any event here.
// If we received this request while stopped, there already was an event for the stop.
// If we received this while running, then doCommand will unblock and trigger the right
// event, using debugger.StopReason because manual stop reason always wins even if we
polinasok marked this conversation as resolved.
Show resolved Hide resolved
// simultaneously receive a manual stop request and hit a breakpoint.
}

// stackFrame represents the index of a frame within
Expand Down Expand Up @@ -1912,7 +1921,7 @@ func (s *Server) resetHandlesForStoppedEvent() {
// asynchornous command has completed setup or was interrupted
// due to an error, so the server is ready to receive new requests.
func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
// TODO(polina): it appears that debugger.Command doesn't close
// TODO(polina): it appears that debugger.Command doesn't always close
// asyncSetupDone (e.g. when having an error next while nexting).
// So we should always close it ourselves just in case.
defer s.asyncCommandDone(asyncSetupDone)
Expand All @@ -1929,14 +1938,18 @@ func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
if err == nil {
stopped.Body.ThreadId = stoppedGoroutineID(state)

file, line := "?", -1
if state.CurrentThread != nil {
file, line = state.CurrentThread.File, state.CurrentThread.Line
}
sr := s.debugger.StopReason()
s.log.Debugf("%q command stopped - reason %q", command, sr)
s.log.Debugf("%q command stopped - reason %q, location %s:%d", command, sr, file, line)
switch sr {
case proc.StopNextFinished:
stopped.Body.Reason = "step"
case proc.StopManual: // triggered by halt
stopped.Body.Reason = "pause"
case proc.StopUnknown: // can happen while stopping
case proc.StopUnknown: // can happen while terminating
stopped.Body.Reason = "unknown"
case proc.StopWatchpoint:
stopped.Body.Reason = "data breakpoint"
Expand Down
99 changes: 73 additions & 26 deletions service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,20 @@ func TestPreSetBreakpoint(t *testing.T) {
// "Continue" is triggered after the response is sent

client.ExpectTerminatedEvent(t)

// Pause request after termination should result in an error.
// But in certain cases this request actually succeeds.
client.PauseRequest(1)
switch r := client.ExpectMessage(t).(type) {
case *dap.ErrorResponse:
if r.Message != "Unable to halt execution" {
t.Errorf("\ngot %#v\nwant Message='Unable to halt execution'", r)
}
case *dap.PauseResponse:
default:
t.Fatalf("Unexpected response type: expect error or pause, got %#v", r)
}

client.DisconnectRequest()
client.ExpectOutputEventProcessExited(t, 0)
client.ExpectOutputEventDetaching(t)
Expand Down Expand Up @@ -2666,15 +2680,8 @@ func TestFatalThrowBreakpoint(t *testing.T) {
})
}

// handleStop covers the standard sequence of reqeusts issued by
// a client at a breakpoint or another non-terminal stop event.
// The details have been tested by other tests,
// so this is just a sanity check.
// Skips line check if line is -1.
func handleStop(t *testing.T, client *daptest.Client, thread int, name string, line int) {
func verifyStopLocation(t *testing.T, client *daptest.Client, thread int, name string, line int) {
t.Helper()
client.ThreadsRequest()
client.ExpectThreadsResponse(t)

client.StackTraceRequest(thread, 0, 20)
st := client.ExpectStackTraceResponse(t)
Expand All @@ -2688,6 +2695,19 @@ func handleStop(t *testing.T, client *daptest.Client, thread int, name string, l
t.Errorf("\ngot %#v\nwant Name=%q", st, name)
}
}
}

// handleStop covers the standard sequence of requests issued by
// a client at a breakpoint or another non-terminal stop event.
// The details have been tested by other tests,
// so this is just a sanity check.
// Skips line check if line is -1.
func handleStop(t *testing.T, client *daptest.Client, thread int, name string, line int) {
t.Helper()
client.ThreadsRequest()
client.ExpectThreadsResponse(t)

verifyStopLocation(t, client, thread, name, line)

client.ScopesRequest(1000)
client.ExpectScopesResponse(t)
Expand Down Expand Up @@ -2985,6 +3005,51 @@ func TestAttachRequest(t *testing.T) {
})
}

func TestPauseAndContinue(t *testing.T) {
runTest(t, "loopprog", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
},
// Set breakpoints
fixture.Source, []int{6},
[]onBreakpoint{{
execute: func() {
verifyStopLocation(t, client, 1, "main.loop", 6)

// Continue resumes all goroutines, so thread id is ignored
client.ContinueRequest(12345)
client.ExpectContinueResponse(t)

time.Sleep(time.Second)

// Halt pauses all goroutines, so thread id is ignored
client.PauseRequest(56789)
// Since we are in async mode while running, we might receive next two messages in either order.
for i := 0; i < 2; i++ {
msg := client.ExpectMessage(t)
switch m := msg.(type) {
case *dap.StoppedEvent:
if m.Body.Reason != "pause" || m.Body.ThreadId != 0 && m.Body.ThreadId != 1 {
t.Errorf("\ngot %#v\nwant ThreadId=0/1 Reason='pause'", m)
}
case *dap.PauseResponse:
default:
t.Fatalf("got %#v, want StoppedEvent or PauseResponse", m)
}
}

// Pause will be a no-op at a pause: there will be no additional stopped events
client.PauseRequest(1)
client.ExpectPauseResponse(t)
},
// The program has an infinite loop, so we must kill it by disconnecting.
disconnect: true,
}})
})
}

func TestUnupportedCommandResponses(t *testing.T) {
var got *dap.ErrorResponse
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
Expand Down Expand Up @@ -3036,24 +3101,6 @@ func TestUnupportedCommandResponses(t *testing.T) {
})
}

func TestRequiredNotYetImplementedResponses(t *testing.T) {
var got *dap.ErrorResponse
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
seqCnt := 1
expectNotYetImplemented := func(cmd string) {
t.Helper()
got = client.ExpectNotYetImplementedErrorResponse(t)
if got.RequestSeq != seqCnt || got.Command != cmd {
t.Errorf("\ngot %#v\nwant RequestSeq=%d Command=%s", got, seqCnt, cmd)
}
seqCnt++
}

client.PauseRequest()
expectNotYetImplemented("pause")
})
}

func TestOptionalNotYetImplementedResponses(t *testing.T) {
var got *dap.ErrorResponse
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
Expand Down