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 accept-multiclient shutdown in remote attach mode #2731

Merged
merged 7 commits into from
Oct 10, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
38 changes: 26 additions & 12 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,9 @@ import (
// error or responding to a (synchronous) DAP disconnect request.
// Once stop is triggered, the goroutine exits.
//
// TODO(polina): add another layer of per-client goroutines to support multiple clients.
// Note that some requests change the process's environment such
// as working directory (for example, see DelveCwd of launch configuration).
// So if we want to reuse this process for multiple debugging sessions
// we need to address that.
// Unlike rpccommon, there is not another layer of per-client
// goroutines here because the dap server does not support
// multiple clients.
//
// (3) Per-request goroutine is started for each asynchronous request
// that resumes execution. We check if target is running already, so
Expand Down Expand Up @@ -248,6 +246,10 @@ func NewServer(config *service.Config) *Server {
logger := logflags.DAPLogger()
logflags.WriteDAPListeningMessage(config.Listener.Addr())
logger.Debug("DAP server pid = ", os.Getpid())
if config.AcceptMulti {
logger.Warn("DAP server does not support accept-multiclient mode")
config.AcceptMulti = false
}
return &Server{
config: &Config{
Config: config,
Expand Down Expand Up @@ -430,7 +432,11 @@ func (s *Session) serveDAPCodec() {
}
s.config.log.Error("DAP error: ", err)
}
s.config.triggerServerStop()
if s.config.AcceptMulti {
s.conn.Close()
} else {
s.config.triggerServerStop()
}
}
return
}
Expand Down Expand Up @@ -1080,13 +1086,23 @@ func (s *Session) stopNoDebugProcess() {
// onDisconnectRequest handles the DisconnectRequest. Per the DAP spec,
// it disconnects the debuggee and signals that the debug adaptor
// (in our case this TCP server) can be terminated.
// TODO(polina): differentiate between single- and multi-client
// server mode when handling requests for debug session shutdown.
func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
defer s.config.triggerServerStop()
s.mu.Lock()
defer s.mu.Unlock()

if s.debugger != nil && s.config.AcceptMulti && !request.Arguments.TerminateDebuggee {
// This is a multi-use server/debugger, so a disconnect request that doesn't
// terminate the debuggee should clean up only the client connection, but not the server.
s.logToConsole("Closing client session, but leaving multi-client DAP server running at " + s.config.Listener.Addr().String())
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
s.conn.Close()
// TODO(polina): should we always restart the target if it is not running?
Copy link
Contributor

Choose a reason for hiding this comment

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

(out of curiosity) why is it useful? Is it how dlv rpc multi-client server works?

Copy link
Collaborator Author

@polinasok polinasok Oct 7, 2021

Choose a reason for hiding this comment

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

dlv rpc doesn't do anything special. The logic is in the terminal client. The terminal client checks in with the server to see if it is multi-client and handles "exit" request differently based on the answer. And you can supply "-c" to ask to continue on exit.

(dlv) exit -c
# Leaves headless instance running

Or you can have the program running, which takes away the terminal prompt, so you must Ctrl-C to pause and get it back, which gives you this:

Would you like to [p]ause the target (returning to Delve's prompt) or [q]uit this client (leaving the target running) [p/q]? 

So you have the following options:

  1. halted => exit + halted -- exit from the prompt
  2. halted => exit + running -- exit -c from the prompt
  3. running => exit + running -- Ctrl-C + option [q]
  4. running => exit + halted -- Ctrl-C + option [p], then "exit"

With this change we would have:

  1. halted => exit + halted -- click Disconnect
  2. halted => exit + running -- click Continue, then Disconnect
  3. running => exit + running -- click Disconnect
  4. running => exit + halted -- click Pause, then Disconnect

This is what vscode-go used to do, until Quoc made this change to always continue. I guess this was the desirable behavior for Cloud Code. It did take away option 1 and 4, which is not great. But in reality we haven't heard complains, have we? Hence the TODO to think about this.

Copy link
Contributor

Choose a reason for hiding this comment

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

which of the four implement "restart the target if the target is not running"?

Copy link
Collaborator Author

@polinasok polinasok Oct 7, 2021

Choose a reason for hiding this comment

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

Option 2 starts with a halted (not running) target and then restarts execution to have it running. Am I misunderstanding your question?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I thought "target is not running" means the target exited. But you meant it's halted. That makes sense and seems like a useful feature to have.

// There is also suspendDebuggee - TBD if vscode supports it.
return
}

defer s.config.triggerServerStop()
var err error
if s.debugger != nil {
// We always kill launched programs.
Expand All @@ -1104,9 +1120,7 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
}
// The debugging session has ended, so we send a terminated event.
s.send(&dap.TerminatedEvent{
Event: *newEvent("terminated"),
})
s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
}

// stopDebugSession is called from Stop (main goroutine) and
Expand Down
68 changes: 63 additions & 5 deletions service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5617,7 +5617,7 @@ func launchDebuggerWithTargetHalted(t *testing.T, fixture string) *debugger.Debu
}

// runTestWithDebugger starts the server and sets its debugger, initializes a debug session,
// runs test, then disconnects. Expects the process at the end of test() to be halted
// runs test, then disconnects. Expects the process at the end of test() to be halted.
func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *daptest.Client)) {
serverStopped := make(chan struct{})
server, _ := startDapServer(t, serverStopped)
Expand All @@ -5637,10 +5637,7 @@ func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *dapt
test(client)

client.DisconnectRequest()
if server.config.AcceptMulti {
// TODO(polina): support multi-client mode
t.Fatal("testing of accept-multiclient not yet supporteed")
} else if server.config.Debugger.AttachPid == 0 { // launched target
if server.config.Debugger.AttachPid == 0 { // launched target
client.ExpectOutputEventDetachingKill(t)
} else { // attached to target
client.ExpectOutputEventDetachingNoKill(t)
Expand Down Expand Up @@ -5681,6 +5678,67 @@ func TestAttachRemoteToHaltedTargetContinueOnEntry(t *testing.T) {

// TODO(polina): Running + stop/continue on entry

// TestMultiClient tests that that remote attach doesn't take down
// the server in multi-client mode unless terminateDebugee is explicitely set.
func TestAttachRemoteMultiClient(t *testing.T) {
closingClientSession := "Closing client session, but leaving multi-client DAP server running at"
detachingAndTerminating := "Detaching and terminating target process"
tests := []struct {
name string
disconnectRequest func(client *daptest.Client)
expect string
}{
{"default", func(c *daptest.Client) { c.DisconnectRequest() }, closingClientSession},
{"terminate=true", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(true) }, detachingAndTerminating},
{"terminate=false", func(c *daptest.Client) { c.DisconnectRequestWithKillOption(false) }, closingClientSession},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
serverStopped := make(chan struct{})
server, forceStop := startDapServer(t, serverStopped)
client := daptest.NewClient(server.listener.Addr().String())
time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
server.sessionMu.Lock()
if server.session == nil {
t.Fatal("dap session is not ready")
}
// DAP server doesn't support accept-multiclient, but we can use this
// hack to test the inner connection logic that can be used by a server that does.
server.session.config.AcceptMulti = true
// TODO(polina): update once the server interface is refactored to take debugger as arg
server.session.debugger = launchDebuggerWithTargetHalted(t, "increment")
server.sessionMu.Unlock()

client.InitializeRequest()
client.ExpectInitializeResponseAndCapabilities(t)

client.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
client.ExpectInitializedEvent(t)
client.ExpectAttachResponse(t)
client.ConfigurationDoneRequest()
client.ExpectStoppedEvent(t)
client.ExpectConfigurationDoneResponse(t)

tc.disconnectRequest(client)
e := client.ExpectOutputEvent(t)
if matched, _ := regexp.MatchString(tc.expect, e.Body.Output); !matched {
t.Errorf("\ngot %#v\nwant Output=%q", e, tc.expect)
}
client.ExpectDisconnectResponse(t)
client.ExpectTerminatedEvent(t)
client.Close()

if tc.expect == closingClientSession {
// At this point a multi-client server is still running, but since
// it is a dap server, it cannot accept another client, so the only
// way to take down the server is too force kill it.
close(forceStop)
}
<-serverStopped
})
}
}

func TestLaunchAttachErrorWhenDebugInProgress(t *testing.T) {
runTestWithDebugger(t, launchDebuggerWithTargetHalted(t, "increment"), func(client *daptest.Client) {
client.AttachRequest(map[string]interface{}{"mode": "local", "processId": 100})
Expand Down