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

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

Merged
merged 11 commits into from
Oct 13, 2021
10 changes: 10 additions & 0 deletions Documentation/usage/dlv_dap.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ 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 --connect flag is a special flag that makes the server operate in reverse mode.
polinasok marked this conversation as resolved.
Show resolved Hide resolved
In this mode, Delve connects to a DAP client listening on host:port,
instead of listening for connections.

```
dlv dap
```

### Options

```
--connect string host:port of the DAP client when running in reverse mode.
polinasok marked this conversation as resolved.
Show resolved Hide resolved
```

### Options inherited from parent commands

```
Expand Down
70 changes: 51 additions & 19 deletions cmd/dlv/cmds/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ var (
// disableASLR is used to disable ASLR
disableASLR bool

// dapConnect is dap subcommand's flag that specifies the address of a DAP client.
// If it is specified, the dap server operates in reverse mode and
// and dials into the client waiting there.
hyangah marked this conversation as resolved.
Show resolved Hide resolved
dapConnect string

// backend selection
backend string

Expand Down Expand Up @@ -185,9 +190,15 @@ to be launched or process to be attached to. The following modes are supported:
- attach + local (attaches to a running process, like 'dlv attach')
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 --connect flag is a special flag that makes the server operate in reverse mode.
In this mode, Delve connects to a DAP client listening on host:port,
instead of listening for connections.`,
Run: dapCmd,
}
dapCommand.Flags().StringVar(&dapConnect, "connect", "", "host:port of the DAP client when running in reverse mode.")

rootCommand.AddCommand(dapCommand)

// 'debug' subcommand.
Expand Down Expand Up @@ -435,26 +446,47 @@ 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
}
var server *dap.Server
disconnectChan := make(chan struct{})
server := dap.NewServer(&service.Config{
Listener: listener,
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
Backend: backend,
Foreground: headless && tty == "",
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
},
CheckLocalConnUser: checkLocalConnUser,
})
defer server.Stop()

if dapConnect == "" {
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("couldn't start listener: %s\n", err)
return 1
}
server = dap.NewServer(&service.Config{
Listener: listener,
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
polinasok marked this conversation as resolved.
Show resolved Hide resolved
Backend: backend,
Foreground: headless && tty == "",
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
},
CheckLocalConnUser: checkLocalConnUser,
})
} else { // reverse mode
hyangah marked this conversation as resolved.
Show resolved Hide resolved
headless = true // TODO(github.com/go-delve/delve/issues/2552): consider the same for the normal mode.
hyangah marked this conversation as resolved.
Show resolved Hide resolved

conn, err := net.Dial("tcp", dapConnect)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This reminded me of the preconnected Listener used by the rpc server. In non-headless (basically with-client) mode, even though the client connection is predefined, the server code is reused as-is. Is something similar perhaps to consider here to arrive at a less nuanced common server denominator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't understand what you mean. There is nothing special here beyond the standard basic tcp connection and I don't see much of commonality beyond that.

preconnected Listener is used to arrange the dlv cli to connect to the server (possibly running in process). There, the client is still a client (the role in the rpc protocol) and the server is what uses service/debugger package. In this reverse mode, the entity that is dialing and plays the role of 'server' in the protocol, is what's using service/debugger package.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In my understanding, the client is the entity that makes the debugging requests and the server is the entity that serves those requests. I don't think it matters which side connects to which side, client to server or server to client. Regardless of the specifics of that initial handshake, the rest of the traffic and meaning of the two sides don't change, do they? That's true for both rpc+terminal case and for dap+special-client case.

In this new mode you are defining, you have a predefined client that you connect to before you call NewServer.
In case of the rpc server + terminal client, we also have a client connection that is predefined and pre-connected before newServer is called. Yet newServer still takes a listener, but that listener is fake. The listener doesn't actually listen as it is already pre-connected to the connection point that was already set up. On the very first call to Accept it will return that pre-existing client connection. And because of that abstraction, newServer and Run can be re-used as-is without any special logic or comments inside of the server itself.

I propose that we do the same here to move connection details outside of the server, keeping it general and consistent with the rpc server, which we ultimately want to merge with under a single command.

listenerpipe.go has ListenerPipe + generic preconnectedListener, which you can reuse more or less ass-is. You can define something like this:

func ListenerFromTCPClient(clientAddr string) net.Listener { 
         conn, err := net.Dial("tcp", clientAddr)
	return &preconnectedListener{conn: conn, closech: make(chan struct{})}
}

Then pass the result to NewServer and not need to modify the dap server code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the proposed approach overcomplicates the code without clear use cases. When the use case that benefits from it is obvious, I am ok to rework this. Until then, I prefer simplicity and less channels and locks.

if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to the DAP proxy server: %v\n", err)
hyangah marked this conversation as resolved.
Show resolved Hide resolved
return 1
}
server = dap.NewReverseServer(&service.Config{
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
Backend: backend,
Foreground: headless && tty == "",
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
},
}, conn)
}
defer server.Stop()
server.Run()
waitForDisconnectSignal(disconnectChan)
return 0
Expand Down
43 changes: 43 additions & 0 deletions cmd/dlv/dlv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"go/types"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -644,6 +645,48 @@ func TestDap(t *testing.T) {
cmd.Wait()
}

// TestDapReverse verifies that a dap-reverse command can be started and shut down.
func TestDapReverse(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", "--connect", listener.Addr().String())
buf := &bytes.Buffer{}
cmd.Stdin = buf
cmd.Stdout = buf
assertNoError(cmd.Start(), t, "start reverse dap instance")

// 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-reverse dialed in successfully")

client := daptest.NewClientWithConn(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)
Expand Down
4 changes: 4 additions & 0 deletions service/dap/daptest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func NewClient(addr string) *Client {
if err != nil {
log.Fatal("dialing:", err)
}
return NewClientWithConn(conn)
}

func NewClientWithConn(conn net.Conn) *Client {
hyangah marked this conversation as resolved.
Show resolved Hide resolved
c := &Client{conn: conn, reader: bufio.NewReader(conn)}
c.seq = 1 // match VS Code numbering
return c
Expand Down
41 changes: 37 additions & 4 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import (
// program termination and failed or closed client connection
// would also result in stopping this single-use server.
//
// The DAP server operates via the following goroutines:
// The DAP server initialized with NewServer
// operates via the following goroutines:
hyangah marked this conversation as resolved.
Show resolved Hide resolved
//
// (1) Main goroutine where the server is created via NewServer(),
// started via Run() and stopped via Stop(). Once the server is
Expand Down Expand Up @@ -87,10 +88,15 @@ import (
// They block on running debugger commands that are interrupted
// when halt is issued while stopping. At that point these goroutines
// wrap-up and exit.
//
// The DAP server set up using NewReverseServer is a special DAP server,
// that is bound to a single net.Conn. Once the connection is closed,
// the server stops.
type Server struct {
// config is all the information necessary to start the debugger and server.
config *service.Config
// listener is used to accept the client connection.
// In reverse mode, this is nil.
hyangah marked this conversation as resolved.
Show resolved Hide resolved
listener net.Listener
// stopTriggered is closed when the server is Stop()-ed.
stopTriggered chan struct{}
Expand Down Expand Up @@ -197,9 +203,27 @@ const (
// disconnects or requests shutdown. Once config.DisconnectChan is closed,
// Server.Stop() must be called to shutdown this single-user server.
func NewServer(config *service.Config) *Server {
return newServer(config, nil, false)
}

// NewReverseServer creates a special DAP server that operates in reverse mode.
// Reverse DAP server is not actually running a TCP server listening on a port,
// but communicates with the provided net.Conn only for a single debug session.
func NewReverseServer(config *service.Config, conn net.Conn) *Server {
hyangah marked this conversation as resolved.
Show resolved Hide resolved
return newServer(config, conn, true)
}

func newServer(config *service.Config, conn net.Conn, inReverseMode bool) *Server {
hyangah marked this conversation as resolved.
Show resolved Hide resolved
logger := logflags.DAPLogger()
logflags.WriteDAPListeningMessage(config.Listener.Addr().String())
logger.Debug("DAP server pid = ", os.Getpid())
if config.Listener != nil {
logflags.WriteDAPListeningMessage(config.Listener.Addr().String())
logger.Debug("DAP server pid = ", os.Getpid())
} else if conn != nil {
logger.Debug("Reverse DAP server pid = ", os.Getpid())
} else {
logger.Fatal("Cannot set up a DAP server without network configuration")
}

return &Server{
config: config,
listener: config.Listener,
Expand All @@ -209,6 +233,7 @@ func NewServer(config *service.Config) *Server {
variableHandles: newVariablesHandlesMap(),
args: defaultArgs,
exceptionErr: nil,
conn: conn,
}
}

Expand Down Expand Up @@ -265,7 +290,10 @@ func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) error {
func (s *Server) Stop() {
s.log.Debug("DAP server stopping...")
close(s.stopTriggered)
_ = s.listener.Close()

if s.listener != nil {
_ = s.listener.Close()
}

s.mu.Lock()
defer s.mu.Unlock()
Expand Down Expand Up @@ -326,6 +354,11 @@ func (s *Server) triggerServerStop() {
// TODO(polina): allow new client connections for new debug sessions,
// so the editor needs to launch delve only once?
func (s *Server) Run() {
if s.listener == nil {
go s.serveDAPCodec()
hyangah marked this conversation as resolved.
Show resolved Hide resolved
return
polinasok marked this conversation as resolved.
Show resolved Hide resolved
}

go func() {
conn, err := s.listener.Accept() // listener is closed in Stop()
if err != nil {
Expand Down