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"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
@ -170,6 +171,35 @@ func (c *Client) ExpectOutputEvent(t *testing.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 {
|
||||
t.Helper()
|
||||
return c.ExpectMessage(t).(*dap.ConfigurationDoneResponse)
|
||||
|
||||
@ -21,4 +21,5 @@ const (
|
||||
UnableToLookupVariable = 2008
|
||||
UnableToEvaluateExpression = 2009
|
||||
// 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
|
||||
// once.
|
||||
func (s *Server) Stop() {
|
||||
s.log.Debug("DAP server stopping...")
|
||||
_ = s.listener.Close()
|
||||
close(s.stopChan)
|
||||
if s.conn != nil {
|
||||
@ -169,6 +170,7 @@ func (s *Server) Stop() {
|
||||
} else {
|
||||
s.stopNoDebugProcess()
|
||||
}
|
||||
s.log.Debug("DAP server stopped")
|
||||
}
|
||||
|
||||
// signalDisconnect closes config.DisconnectChan if not nil, which
|
||||
@ -421,6 +423,15 @@ func (s *Server) send(message dap.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) {
|
||||
// TODO(polina): Respond with an error if debug session is in progress?
|
||||
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.
|
||||
if err := cmd.Wait(); err != nil {
|
||||
s.log.Debugf("program exited: %v", err)
|
||||
s.log.Debugf("program exited with error: %v", err)
|
||||
}
|
||||
|
||||
stopped := false
|
||||
s.mu.Lock()
|
||||
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()
|
||||
|
||||
if !stopped {
|
||||
s.logToConsole(proc.ErrProcessExited{Pid: cmd.ProcessState.Pid(), Status: cmd.ProcessState.ExitCode()}.Error())
|
||||
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
|
||||
}
|
||||
return
|
||||
@ -613,8 +624,16 @@ func (s *Server) stopNoDebugProcess() {
|
||||
s.noDebugProcess = nil
|
||||
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.
|
||||
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.
|
||||
}
|
||||
}
|
||||
@ -633,30 +652,42 @@ func isValidLaunchMode(launchMode interface{}) bool {
|
||||
// it disconnects the debuggee and signals that the debug adaptor
|
||||
// (in our case this TCP server) can be terminated.
|
||||
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 {
|
||||
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
||||
state, err := s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *proc.ErrProcessExited:
|
||||
s.log.Debug(err)
|
||||
exited = err
|
||||
default:
|
||||
s.log.Error(err)
|
||||
}
|
||||
} else if state.Exited {
|
||||
exited = proc.ErrProcessExited{Pid: s.debugger.ProcessPid(), Status: state.ExitStatus}
|
||||
}
|
||||
// We always kill launched programs
|
||||
kill := s.config.Debugger.AttachPid == 0
|
||||
// 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.
|
||||
if request.Arguments.TerminateDebuggee {
|
||||
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)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *proc.ErrProcessExited:
|
||||
s.log.Debug(err)
|
||||
exited = err
|
||||
s.logToConsole(exited.Error())
|
||||
default:
|
||||
s.log.Error(err)
|
||||
}
|
||||
@ -664,6 +695,11 @@ func (s *Server) onDisconnectRequest(request *dap.DisconnectRequest) {
|
||||
} else {
|
||||
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.
|
||||
s.signalDisconnect()
|
||||
|
||||
@ -113,6 +113,8 @@ func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.F
|
||||
// : 12 << continue
|
||||
// - Program runs to completion : << terminated event
|
||||
// : 13 >> disconnect
|
||||
// : << output event (Process exited)
|
||||
// : << output event (Detaching)
|
||||
// : 13 << disconnect
|
||||
// This test exhaustively tests Seq and RequestSeq on all messages from the
|
||||
// server. Other tests do not necessarily need to repeat all these checks.
|
||||
@ -222,6 +224,14 @@ func TestLaunchStopOnEntry(t *testing.T) {
|
||||
|
||||
// 13 >> disconnect, << disconnect
|
||||
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)
|
||||
if dResp.Seq != 0 || dResp.RequestSeq != 13 {
|
||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=13", dResp)
|
||||
@ -344,6 +354,10 @@ func TestAttachStopOnEntry(t *testing.T) {
|
||||
|
||||
// 12 >> disconnect, << disconnect
|
||||
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)
|
||||
if dResp.Seq != 0 || dResp.RequestSeq != 12 {
|
||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=12", dResp)
|
||||
@ -398,6 +412,8 @@ func TestContinueOnEntry(t *testing.T) {
|
||||
|
||||
// 7 >> disconnect, << disconnect
|
||||
client.DisconnectRequest()
|
||||
client.ExpectOutputEventProcessExited(t, 0)
|
||||
client.ExpectOutputEventDetaching(t)
|
||||
dResp := client.ExpectDisconnectResponse(t)
|
||||
if dResp.Seq != 0 || dResp.RequestSeq != 7 {
|
||||
t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=7", dResp)
|
||||
@ -511,6 +527,8 @@ func TestPreSetBreakpoint(t *testing.T) {
|
||||
|
||||
client.ExpectTerminatedEvent(t)
|
||||
client.DisconnectRequest()
|
||||
client.ExpectOutputEventProcessExited(t, 0)
|
||||
client.ExpectOutputEventDetaching(t)
|
||||
client.ExpectDisconnectResponse(t)
|
||||
})
|
||||
}
|
||||
@ -2019,8 +2037,8 @@ func TestEvaluateCallRequest(t *testing.T) {
|
||||
}
|
||||
// Call can exit
|
||||
client.EvaluateRequest("call callexit()", 1000, "this context will be ignored")
|
||||
client.ExpectTerminatedEvent(t)
|
||||
},
|
||||
terminated: 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
|
||||
// execution. Otherwise, a continue will be issued and the
|
||||
// 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 {
|
||||
execute func()
|
||||
disconnect bool
|
||||
terminated bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
onBP.execute()
|
||||
if onBP.disconnect {
|
||||
if onBP.terminated {
|
||||
client.ExpectTerminatedEvent(t)
|
||||
}
|
||||
client.DisconnectRequestWithKillOption(true)
|
||||
if onBP.terminated {
|
||||
client.ExpectOutputEventProcessExited(t, 0)
|
||||
client.ExpectOutputEventDetaching(t)
|
||||
} else {
|
||||
client.ExpectOutputEventDetachingKill(t)
|
||||
}
|
||||
client.ExpectDisconnectResponse(t)
|
||||
return
|
||||
}
|
||||
@ -2488,6 +2518,12 @@ func runDebugSessionWithBPs(t *testing.T, client *daptest.Client, cmd string, cm
|
||||
client.ExpectTerminatedEvent(t)
|
||||
}
|
||||
client.DisconnectRequestWithKillOption(true)
|
||||
if cmd == "launch" {
|
||||
client.ExpectOutputEventProcessExited(t, 0)
|
||||
client.ExpectOutputEventDetaching(t)
|
||||
} else if cmd == "attach" {
|
||||
client.ExpectOutputEventDetachingKill(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) {
|
||||
runNoDebugDebugSession(t, client, func() {
|
||||
client.LaunchRequestWithArgs(map[string]interface{}{
|
||||
"noDebug": true,
|
||||
"mode": "", /*"debug" by default*/
|
||||
"mode": "debug",
|
||||
"program": fixture.Source,
|
||||
"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
|
||||
// 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.ExpectInitializeResponse(t)
|
||||
|
||||
@ -2590,6 +2638,7 @@ func runNoDebugDebugSession(t *testing.T, client *daptest.Client, cmdRequest fun
|
||||
// noDebug mode applies only to "launch" requests.
|
||||
client.ExpectLaunchResponse(t)
|
||||
|
||||
client.ExpectOutputEventProcessExited(t, status)
|
||||
client.ExpectTerminatedEvent(t)
|
||||
client.DisconnectRequestWithKillOption(true)
|
||||
client.ExpectDisconnectResponse(t)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user