Skip to content

Commit

Permalink
cmd/dlv: add --client-addr flag to run dap with a predefined client (#…
Browse files Browse the repository at this point in the history
…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).
  • Loading branch information
hyangah authored Oct 13, 2021
1 parent 6c4029c commit 98a0bcf
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 19 deletions.
7 changes: 6 additions & 1 deletion Documentation/usage/dlv_dap.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ 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]
```

### Options

```
-h, --help help for dap
--client-addr string host:port where the DAP client is waiting for the DAP server to dial in
-h, --help help for dap
```

### Options inherited from parent commands
Expand Down
48 changes: 37 additions & 11 deletions cmd/dlv/cmds/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand All @@ -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.Run()
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
}()
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"
"os/user"
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions service/dap/daptest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 41 additions & 7 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
logflags.WriteDAPListeningMessage(config.Listener.Addr())
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")
Expand Down Expand Up @@ -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 run goroutine is blocked on accept, this will unblock it.
_ = s.listener.Close()

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()
Expand Down Expand Up @@ -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 {
Expand All @@ -399,13 +416,30 @@ func (s *Server) Run() {
return
}
}
s.sessionMu.Lock()
s.session = NewSession(conn, s.config) // closed in Stop()
s.sessionMu.Unlock()
s.session.serveDAPCodec()
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
// until it encounters an error or EOF, when it sends
// a disconnect signal and returns.
Expand Down

0 comments on commit 98a0bcf

Please sign in to comment.