cmd/dlv: add --client-addr flag to run dap with a predefined client (#2568)

This adds a new `--client-addr=host:port` flag to `dlv dap`.
If it is supplied, the dap process will dial into the tcp port where
a DAP client is waiting, and work with only the DAP client.
The DAP client is supposed to start the normal DAP message
exchange starting with the 'initialize' request after the dlv dap
process dials in and the connection is set up. 

VS Code Go extension plans to use this mode for

* reliably detecting `dlv dap` readiness. Currently it depends on
watching the log stream. After this PR, it can listen on a network port.
* running `dlv dap` from any terminal (part of RunInTerminal workflow
implementation).
This commit is contained in:
Hyang-Ah Hana Kim
2021-10-13 14:43:47 -04:00
committed by GitHub
parent 6c4029c765
commit 98a0bcf772
5 changed files with 133 additions and 19 deletions

View File

@ -20,6 +20,10 @@ The server does not yet accept multiple client connections (--accept-multiclient
While --continue is not supported, stopOnEntry launch/attach attribute can be used to control if
execution is resumed at the start of the debug session.
The --client-addr flag is a special flag that makes the server initiate a debug session
by dialing in to the host:port where a DAP client is waiting. This server process
will exit when the debug session ends.
```
dlv dap [flags]
```
@ -27,6 +31,7 @@ dlv dap [flags]
### Options
```
--client-addr string host:port where the DAP client is waiting for the DAP server to dial in
-h, --help help for dap
```

View File

@ -62,6 +62,11 @@ var (
// disableASLR is used to disable ASLR
disableASLR bool
// dapClientAddr is dap subcommand's flag that specifies the address of a DAP client.
// If it is specified, the dap server starts a debug session by dialing to the client.
// The dap server will serve only for the debug session.
dapClientAddr string
// backend selection
backend string
@ -191,9 +196,15 @@ Program and output binary paths will be interpreted relative to dlv's working di
The server does not yet accept multiple client connections (--accept-multiclient).
While --continue is not supported, stopOnEntry launch/attach attribute can be used to control if
execution is resumed at the start of the debug session.`,
execution is resumed at the start of the debug session.
The --client-addr flag is a special flag that makes the server initiate a debug session
by dialing in to the host:port where a DAP client is waiting. This server process
will exit when the debug session ends.`,
Run: dapCmd,
}
dapCommand.Flags().StringVar(&dapClientAddr, "client-addr", "", "host:port where the DAP client is waiting for the DAP server to dial in")
// TODO(polina): support --tty when dlv dap allows to launch a program from command-line
rootCommand.AddCommand(dapCommand)
@ -451,14 +462,8 @@ func dapCmd(cmd *cobra.Command, args []string) {
fmt.Fprintf(os.Stderr, "Warning: program flags ignored with dap; specify via launch/attach request instead\n")
}
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("couldn't start listener: %s\n", err)
return 1
}
disconnectChan := make(chan struct{})
server := dap.NewServer(&service.Config{
Listener: listener,
config := &service.Config{
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
Backend: backend,
@ -467,10 +472,31 @@ func dapCmd(cmd *cobra.Command, args []string) {
CheckGoVersion: checkGoVersion,
},
CheckLocalConnUser: checkLocalConnUser,
})
defer server.Stop()
}
var conn net.Conn
if dapClientAddr == "" {
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("couldn't start listener: %s\n", err)
return 1
}
config.Listener = listener
} else { // with a predetermined client.
var err error
conn, err = net.Dial("tcp", dapClientAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to the DAP client: %v\n", err)
return 1
}
}
server := dap.NewServer(config)
defer server.Stop()
if conn == nil {
server.Run()
} else { // work with a predetermined client.
server.RunWithClient(conn)
}
waitForDisconnectSignal(disconnectChan)
return 0
}()

View File

@ -10,6 +10,7 @@ import (
"go/types"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"os/user"
@ -690,6 +691,48 @@ func TestDap(t *testing.T) {
cmd.Wait()
}
// TestDapWithClient tests dlv dap --client-addr can be started and shut down.
func TestDapWithClient(t *testing.T) {
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("cannot setup listener required for testing: %v", err)
}
defer listener.Close()
dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir)
cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr", listener.Addr().String())
buf := &bytes.Buffer{}
cmd.Stdin = buf
cmd.Stdout = buf
assertNoError(cmd.Start(), t, "start dlv dap process with --client-addr flag")
// Wait for the connection.
conn, err := listener.Accept()
if err != nil {
cmd.Process.Kill() // release the port
t.Fatalf("Failed to get connection: %v", err)
}
t.Log("dlv dap process dialed in successfully")
client := daptest.NewClientFromConn(conn)
client.InitializeRequest()
client.ExpectInitializeResponse(t)
// Close the connection.
if err := conn.Close(); err != nil {
cmd.Process.Kill()
t.Fatalf("Failed to get connection: %v", err)
}
// Connection close should trigger dlv-reverse command's normal exit.
if err := cmd.Wait(); err != nil {
cmd.Process.Kill()
t.Fatalf("command failed: %v\n%s\n%v", err, buf.Bytes(), cmd.Process.Pid)
}
}
func TestTrace(t *testing.T) {
dlvbin, tmpdir := getDlvBin(t)
defer os.RemoveAll(tmpdir)

View File

@ -37,6 +37,12 @@ func NewClient(addr string) *Client {
if err != nil {
log.Fatal("dialing:", err)
}
return NewClientFromConn(conn)
}
// NewClientFromConn creates a new Client with the given TCP connection.
// Call Close to close the connection.
func NewClientFromConn(conn net.Conn) *Client {
c := &Client{conn: conn, reader: bufio.NewReader(conn)}
c.seq = 1 // match VS Code numbering
return c

View File

@ -97,6 +97,7 @@ type Server struct {
// config is all the information necessary to start the debugger and server.
config *Config
// listener is used to accept the client connection.
// When working with a predetermined client, this is nil.
listener net.Listener
// session is the debug session that comes with an client connection.
session *Session
@ -242,9 +243,17 @@ var (
// it will be closed by the server when the client fails to connect,
// disconnects or requests shutdown. Once config.DisconnectChan is closed,
// Server.Stop() must be called to shutdown this single-user server.
//
// NewServer can be used to create a special DAP Server that works
// only with a predetermined client. In that case, config.Listener is
// nil and its RunWithClient must be used instead of Run.
func NewServer(config *service.Config) *Server {
logger := logflags.DAPLogger()
if config.Listener != nil {
logflags.WriteDAPListeningMessage(config.Listener.Addr())
} else {
logger.Debug("DAP server for a predetermined client")
}
logger.Debug("DAP server pid = ", os.Getpid())
if config.AcceptMulti {
logger.Warn("DAP server does not support accept-multiclient mode")
@ -308,8 +317,11 @@ func (s *Server) Stop() {
s.config.log.Debug("DAP server stopping...")
defer s.config.log.Debug("DAP server stopped")
close(s.config.stopTriggered)
if s.listener != nil {
// If run goroutine is blocked on accept, this will unblock it.
_ = s.listener.Close()
}
s.sessionMu.Lock()
defer s.sessionMu.Unlock()
@ -381,6 +393,11 @@ func (c *Config) triggerServerStop() {
// So if we want to reuse this server for multiple independent debugging sessions
// we need to take that into consideration.
func (s *Server) Run() {
if s.listener == nil {
s.config.log.Fatal("Misconfigured server: no Listener is configured.")
return
}
go func() {
conn, err := s.listener.Accept() // listener is closed in Stop()
if err != nil {
@ -399,11 +416,28 @@ func (s *Server) Run() {
return
}
}
s.runSession(conn)
}()
}
func (s *Server) runSession(conn io.ReadWriteCloser) {
s.sessionMu.Lock()
s.session = NewSession(conn, s.config) // closed in Stop()
s.sessionMu.Unlock()
s.session.serveDAPCodec()
}()
}
// RunWithClient is similar to Run but works only with an already established
// connection instead of waiting on the listener to accept a new client.
// RunWithClient takes ownership of conn. Debugger won't be started
// until a launch/attach request is received over the connection.
func (s *Server) RunWithClient(conn net.Conn) {
if s.listener != nil {
s.config.log.Fatal("RunWithClient must not be used when the Server is configured with a Listener")
return
}
s.config.log.Debugf("Connected to the client at %s", conn.RemoteAddr())
go s.runSession(conn)
}
// serveDAPCodec reads and decodes requests from the client