service/dap: delay disconnect response and log teardown progress (#2427)
Co-authored-by: Polina Sokolova <polinasok@users.noreply.github.com>
This commit is contained in:
parent
8b20609227
commit
aa426a2e50
@ -8,6 +8,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-dap"
|
"github.com/google/go-dap"
|
||||||
@ -170,6 +171,35 @@ func (c *Client) ExpectOutputEvent(t *testing.T) *dap.OutputEvent {
|
|||||||
return c.ExpectMessage(t).(*dap.OutputEvent)
|
return c.ExpectMessage(t).(*dap.OutputEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
e := c.ExpectMessage(t).(*dap.OutputEvent)
|
||||||
|
if matched, _ := regexp.MatchString(want, e.Body.Output); !matched {
|
||||||
|
t.Errorf("\ngot %#v\nwant Output=%q", e, want)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventProcessExited(t *testing.T, status int) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
return c.ExpectOutputEventRegex(t, fmt.Sprintf(`Process [0-9]+ has exited with status %d\n`, status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventDetaching(t *testing.T) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
return c.ExpectOutputEventRegex(t, `Detaching\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventDetachingKill(t *testing.T) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
return c.ExpectOutputEventRegex(t, `Detaching and terminating target process\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventDetachingNoKill(t *testing.T) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
return c.ExpectOutputEventRegex(t, `Detaching without terminating target process\n`)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) ExpectConfigurationDoneResponse(t *testing.T) *dap.ConfigurationDoneResponse {
|
func (c *Client) ExpectConfigurationDoneResponse(t *testing.T) *dap.ConfigurationDoneResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return c.ExpectMessage(t).(*dap.ConfigurationDoneResponse)
|
return c.ExpectMessage(t).(*dap.ConfigurationDoneResponse)
|
||||||
|
|||||||
@ -21,4 +21,5 @@ const (
|
|||||||
UnableToLookupVariable = 2008
|
UnableToLookupVariable = 2008
|
||||||
UnableToEvaluateExpression = 2009
|
UnableToEvaluateExpression = 2009
|
||||||
// Add more codes as we support more requests
|
// Add more codes as we support more requests
|
||||||
|
DisconnectError = 5000
|
||||||
)
|
)
|
||||||
|
|||||||
@ -147,6 +147,7 @@ func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) {
|
|||||||
// process if it was launched by it. This method mustn't be called more than
|
// process if it was launched by it. This method mustn't be called more than
|
||||||
// once.
|
// once.
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
|
s.log.Debug("DAP server stopping...")
|
||||||
_ = s.listener.Close()
|
_ = s.listener.Close()
|
||||||
close(s.stopChan)
|
close(s.stopChan)
|
||||||
if s.conn != nil {
|
if s.conn != nil {
|
||||||
@ -169,6 +170,7 @@ func (s *Server) Stop() {
|
|||||||
} else {
|
} else {
|
||||||
s.stopNoDebugProcess()
|
s.stopNoDebugProcess()
|
||||||
}
|
}
|
||||||
|
s.log.Debug("DAP server stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
// signalDisconnect closes config.DisconnectChan if not nil, which
|
// signalDisconnect closes config.DisconnectChan if not nil, which
|
||||||
@ -421,6 +423,15 @@ func (s *Server) send(message dap.Message) {
|
|||||||
_ = dap.WriteProtocolMessage(s.conn, message)
|
_ = dap.WriteProtocolMessage(s.conn, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) logToConsole(msg string) {
|
||||||
|
s.send(&dap.OutputEvent{
|
||||||
|
Event: *newEvent("output"),
|
||||||
|
Body: dap.OutputEventBody{
|
||||||
|
Output: msg + "\n",
|
||||||
|
Category: "console",
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
|
func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
|
||||||
// TODO(polina): Respond with an error if debug session is in progress?
|
// TODO(polina): Respond with an error if debug session is in progress?
|
||||||
response := &dap.InitializeResponse{Response: *newResponse(request.Request)}
|
response := &dap.InitializeResponse{Response: *newResponse(request.Request)}
|
||||||
@ -568,9 +579,8 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
|
|
||||||
// Then, block until the program terminates or is stopped.
|
// Then, block until the program terminates or is stopped.
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
s.log.Debugf("program exited: %v", err)
|
s.log.Debugf("program exited with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stopped := false
|
stopped := false
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
stopped = s.noDebugProcess == nil // if it was stopped, this should be nil.
|
stopped = s.noDebugProcess == nil // if it was stopped, this should be nil.
|
||||||
@ -578,6 +588,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if !stopped {
|
if !stopped {
|
||||||
|
s.logToConsole(proc.ErrProcessExited{Pid: cmd.ProcessState.Pid(), Status: cmd.ProcessState.ExitCode()}.Error())
|
||||||
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
|
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -613,8 +624,16 @@ func (s *Server) stopNoDebugProcess() {
|
|||||||
s.noDebugProcess = nil
|
s.noDebugProcess = nil
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
// We already handled termination or there was never a process
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ProcessState.Exited() {
|
||||||
|
s.logToConsole(proc.ErrProcessExited{Pid: p.ProcessState.Pid(), Status: p.ProcessState.ExitCode()}.Error())
|
||||||
|
} else {
|
||||||
// TODO(hyangah): gracefully terminate the process and its children processes.
|
// TODO(hyangah): gracefully terminate the process and its children processes.
|
||||||
if p != nil && !p.ProcessState.Exited() {
|
s.logToConsole(fmt.Sprintf("Terminating process %d", p.Process.Pid))
|
||||||
p.Process.Kill() // Don't check error. Process killing and self-termination may race.
|
p.Process.Kill() // Don't check error. Process killing and self-termination may race.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -633,30 +652,42 @@ func isValidLaunchMode(launchMode interface{}) bool {
|
|||||||
// it disconnects the debuggee and signals that the debug adaptor
|
// it disconnects the debuggee and signals that the debug adaptor
|
||||||
// (in our case this TCP server) can be terminated.
|
// (in our case this TCP server) can be terminated.
|
||||||
func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
|
func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
|
||||||
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
|
var err error
|
||||||
|
var exited error
|
||||||
if s.debugger != nil {
|
if s.debugger != nil {
|
||||||
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
state, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *proc.ErrProcessExited:
|
case *proc.ErrProcessExited:
|
||||||
s.log.Debug(err)
|
exited = err
|
||||||
default:
|
default:
|
||||||
s.log.Error(err)
|
s.log.Error(err)
|
||||||
}
|
}
|
||||||
|
} else if state.Exited {
|
||||||
|
exited = proc.ErrProcessExited{Pid: s.debugger.ProcessPid(), Status: state.ExitStatus}
|
||||||
}
|
}
|
||||||
// We always kill launched programs
|
// We always kill launched programs
|
||||||
kill := s.config.Debugger.AttachPid == 0
|
kill := s.config.Debugger.AttachPid == 0
|
||||||
// In case of attach, we leave the program
|
// In case of attach, we leave the program
|
||||||
// running but default, which can be
|
// running by default, which can be
|
||||||
// overridden by an explicit request to terminate.
|
// overridden by an explicit request to terminate.
|
||||||
if request.Arguments.TerminateDebuggee {
|
if request.Arguments.TerminateDebuggee {
|
||||||
kill = true
|
kill = true
|
||||||
}
|
}
|
||||||
|
if exited != nil {
|
||||||
|
s.logToConsole(exited.Error())
|
||||||
|
s.logToConsole("Detaching")
|
||||||
|
} else if kill {
|
||||||
|
s.logToConsole("Detaching and terminating target process")
|
||||||
|
} else {
|
||||||
|
s.logToConsole("Detaching without terminating target processs")
|
||||||
|
}
|
||||||
err = s.debugger.Detach(kill)
|
err = s.debugger.Detach(kill)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case *proc.ErrProcessExited:
|
case *proc.ErrProcessExited:
|
||||||
s.log.Debug(err)
|
exited = err
|
||||||
|
s.logToConsole(exited.Error())
|
||||||
default:
|
default:
|
||||||
s.log.Error(err)
|
s.log.Error(err)
|
||||||
}
|
}
|
||||||
@ -664,6 +695,11 @@ func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
|
|||||||
} else {
|
} else {
|
||||||
s.stopNoDebugProcess()
|
s.stopNoDebugProcess()
|
||||||
}
|
}
|
||||||
|
if err != nil && exited == nil {
|
||||||
|
s.sendErrorResponse(request.Request, DisconnectError, "Error while disconnecting", err.Error())
|
||||||
|
} else {
|
||||||
|
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(polina): make thread-safe when handlers become asynchronous.
|
// TODO(polina): make thread-safe when handlers become asynchronous.
|
||||||
s.signalDisconnect()
|
s.signalDisconnect()
|
||||||
|
|||||||
@ -113,6 +113,8 @@ func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.F
|
|||||||
// : 12 << continue
|
// : 12 << continue
|
||||||
// - Program runs to completion : << terminated event
|
// - Program runs to completion : << terminated event
|
||||||
// : 13 >> disconnect
|
// : 13 >> disconnect
|
||||||
|
// : << output event (Process exited)
|
||||||
|
// : << output event (Detaching)
|
||||||
// : 13 << disconnect
|
// : 13 << disconnect
|
||||||
// This test exhaustively tests Seq and RequestSeq on all messages from the
|
// This test exhaustively tests Seq and RequestSeq on all messages from the
|
||||||
// server. Other tests do not necessarily need to repeat all these checks.
|
// server. Other tests do not necessarily need to repeat all these checks.
|
||||||
@ -222,6 +224,14 @@ func TestLaunchStopOnEntry(t *testing.T) {
|
|||||||
|
|
||||||
// 13 >> disconnect, << disconnect
|
// 13 >> disconnect, << disconnect
|
||||||
client.DisconnectRequest()
|
client.DisconnectRequest()
|
||||||
|
oep := client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
if oep.Seq != 0 || oep.Body.Category != "console" {
|
||||||
|
t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oep)
|
||||||
|
}
|
||||||
|
oed := client.ExpectOutputEventDetaching(t)
|
||||||
|
if oed.Seq != 0 || oep.Body.Category != "console" {
|
||||||
|
t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oed)
|
||||||
|
}
|
||||||
dResp := client.ExpectDisconnectResponse(t)
|
dResp := client.ExpectDisconnectResponse(t)
|
||||||
if dResp.Seq != 0 || dResp.RequestSeq != 13 {
|
if dResp.Seq != 0 || dResp.RequestSeq != 13 {
|
||||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=13", dResp)
|
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=13", dResp)
|
||||||
@ -344,6 +354,10 @@ func TestAttachStopOnEntry(t *testing.T) {
|
|||||||
|
|
||||||
// 12 >> disconnect, << disconnect
|
// 12 >> disconnect, << disconnect
|
||||||
client.DisconnectRequestWithKillOption(true)
|
client.DisconnectRequestWithKillOption(true)
|
||||||
|
oed := client.ExpectOutputEventDetachingKill(t)
|
||||||
|
if oed.Seq != 0 || oed.Body.Category != "console" {
|
||||||
|
t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oed)
|
||||||
|
}
|
||||||
dResp := client.ExpectDisconnectResponse(t)
|
dResp := client.ExpectDisconnectResponse(t)
|
||||||
if dResp.Seq != 0 || dResp.RequestSeq != 12 {
|
if dResp.Seq != 0 || dResp.RequestSeq != 12 {
|
||||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=12", dResp)
|
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=12", dResp)
|
||||||
@ -398,6 +412,8 @@ func TestContinueOnEntry(t *testing.T) {
|
|||||||
|
|
||||||
// 7 >> disconnect, << disconnect
|
// 7 >> disconnect, << disconnect
|
||||||
client.DisconnectRequest()
|
client.DisconnectRequest()
|
||||||
|
client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
client.ExpectOutputEventDetaching(t)
|
||||||
dResp := client.ExpectDisconnectResponse(t)
|
dResp := client.ExpectDisconnectResponse(t)
|
||||||
if dResp.Seq != 0 || dResp.RequestSeq != 7 {
|
if dResp.Seq != 0 || dResp.RequestSeq != 7 {
|
||||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7", dResp)
|
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7", dResp)
|
||||||
@ -511,6 +527,8 @@ func TestPreSetBreakpoint(t *testing.T) {
|
|||||||
|
|
||||||
client.ExpectTerminatedEvent(t)
|
client.ExpectTerminatedEvent(t)
|
||||||
client.DisconnectRequest()
|
client.DisconnectRequest()
|
||||||
|
client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
client.ExpectOutputEventDetaching(t)
|
||||||
client.ExpectDisconnectResponse(t)
|
client.ExpectDisconnectResponse(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -2019,8 +2037,8 @@ func TestEvaluateCallRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Call can exit
|
// Call can exit
|
||||||
client.EvaluateRequest("call callexit()", 1000, "this context will be ignored")
|
client.EvaluateRequest("call callexit()", 1000, "this context will be ignored")
|
||||||
client.ExpectTerminatedEvent(t)
|
|
||||||
},
|
},
|
||||||
|
terminated: true,
|
||||||
disconnect: true,
|
disconnect: true,
|
||||||
}})
|
}})
|
||||||
})
|
})
|
||||||
@ -2428,9 +2446,12 @@ func handleStop(t *testing.T, client *daptest.Client, thread int, name string, l
|
|||||||
// disconnect is true, the test harness will abort the program
|
// disconnect is true, the test harness will abort the program
|
||||||
// execution. Otherwise, a continue will be issued and the
|
// execution. Otherwise, a continue will be issued and the
|
||||||
// program will continue to the next breakpoint or termination.
|
// program will continue to the next breakpoint or termination.
|
||||||
|
// If terminated is true, we expect requests at this breakpoint
|
||||||
|
// to result in termination.
|
||||||
type onBreakpoint struct {
|
type onBreakpoint struct {
|
||||||
execute func()
|
execute func()
|
||||||
disconnect bool
|
disconnect bool
|
||||||
|
terminated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDebugSessionWithBPs is a helper for executing the common init and shutdown
|
// runDebugSessionWithBPs is a helper for executing the common init and shutdown
|
||||||
@ -2475,7 +2496,16 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
|
|||||||
client.ExpectStoppedEvent(t)
|
client.ExpectStoppedEvent(t)
|
||||||
onBP.execute()
|
onBP.execute()
|
||||||
if onBP.disconnect {
|
if onBP.disconnect {
|
||||||
|
if onBP.terminated {
|
||||||
|
client.ExpectTerminatedEvent(t)
|
||||||
|
}
|
||||||
client.DisconnectRequestWithKillOption(true)
|
client.DisconnectRequestWithKillOption(true)
|
||||||
|
if onBP.terminated {
|
||||||
|
client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
client.ExpectOutputEventDetaching(t)
|
||||||
|
} else {
|
||||||
|
client.ExpectOutputEventDetachingKill(t)
|
||||||
|
}
|
||||||
client.ExpectDisconnectResponse(t)
|
client.ExpectDisconnectResponse(t)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2488,6 +2518,12 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
|
|||||||
client.ExpectTerminatedEvent(t)
|
client.ExpectTerminatedEvent(t)
|
||||||
}
|
}
|
||||||
client.DisconnectRequestWithKillOption(true)
|
client.DisconnectRequestWithKillOption(true)
|
||||||
|
if cmd == "launch" {
|
||||||
|
client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
client.ExpectOutputEventDetaching(t)
|
||||||
|
} else if cmd == "attach" {
|
||||||
|
client.ExpectOutputEventDetachingKill(t)
|
||||||
|
}
|
||||||
client.ExpectDisconnectResponse(t)
|
client.ExpectDisconnectResponse(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2567,21 +2603,33 @@ func TestLaunchRequestDefaults(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLaunchRequestNoDebug(t *testing.T) {
|
func TestLaunchRequestNoDebug_GoodStatus(t *testing.T) {
|
||||||
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
|
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
runNoDebugDebugSession(t, client, func() {
|
runNoDebugDebugSession(t, client, func() {
|
||||||
client.LaunchRequestWithArgs(map[string]interface{}{
|
client.LaunchRequestWithArgs(map[string]interface{}{
|
||||||
"noDebug": true,
|
"noDebug": true,
|
||||||
"mode": "", /*"debug" by default*/
|
"mode": "debug",
|
||||||
"program": fixture.Source,
|
"program": fixture.Source,
|
||||||
"output": cleanExeName("__mybin")})
|
"output": cleanExeName("__mybin")})
|
||||||
}, fixture.Source, []int{8})
|
}, fixture.Source, []int{8}, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLaunchRequestNoDebug_BadStatus(t *testing.T) {
|
||||||
|
runTest(t, "issue1101", func(client *daptest.Client, fixture protest.Fixture) {
|
||||||
|
runNoDebugDebugSession(t, client, func() {
|
||||||
|
client.LaunchRequestWithArgs(map[string]interface{}{
|
||||||
|
"noDebug": true,
|
||||||
|
"mode": "debug",
|
||||||
|
"program": fixture.Source,
|
||||||
|
"output": cleanExeName("__mybin")})
|
||||||
|
}, fixture.Source, []int{8}, 2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// runNoDebugDebugSession tests the session started with noDebug=true runs uninterrupted
|
// runNoDebugDebugSession tests the session started with noDebug=true runs uninterrupted
|
||||||
// even when breakpoint is set.
|
// even when breakpoint is set.
|
||||||
func runNoDebugDebugSession(t *testing.T, client *daptest.Client, cmdRequest func(), source string, breakpoints []int) {
|
func runNoDebugDebugSession(t *testing.T, client *daptest.Client, cmdRequest func(), source string, breakpoints []int, status int) {
|
||||||
client.InitializeRequest()
|
client.InitializeRequest()
|
||||||
client.ExpectInitializeResponse(t)
|
client.ExpectInitializeResponse(t)
|
||||||
|
|
||||||
@ -2590,6 +2638,7 @@ func runNoDebugDebugSession(t *testing.T, client *daptest.Client, cmdRequest fun
|
|||||||
// noDebug mode applies only to "launch" requests.
|
// noDebug mode applies only to "launch" requests.
|
||||||
client.ExpectLaunchResponse(t)
|
client.ExpectLaunchResponse(t)
|
||||||
|
|
||||||
|
client.ExpectOutputEventProcessExited(t, status)
|
||||||
client.ExpectTerminatedEvent(t)
|
client.ExpectTerminatedEvent(t)
|
||||||
client.DisconnectRequestWithKillOption(true)
|
client.DisconnectRequestWithKillOption(true)
|
||||||
client.ExpectDisconnectResponse(t)
|
client.ExpectDisconnectResponse(t)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user