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
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")
polinasok marked this conversation as resolved.
Show resolved Hide resolved

// 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
50 changes: 43 additions & 7 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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 @@ -240,10 +241,19 @@ 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())
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be moved to Run(). Technically you are not listening until then anyway. Maybe it would even be cleaner to have Run() take listener as arg. I am not yet full convinced we should strip the server layer in this case and if we keep it.

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 agree.

The only issue is the log from WriteDAPListeningMessage must be the first log output. So, it's better to keep it close to where the logger is created. We can move logger creation to Run too. What do you think?

} else {
logger.Debug("DAP server for a predetermined client")
}
logger.Debug("DAP server pid = ", os.Getpid())

return &Server{
config: &Config{
Config: config,
Expand Down Expand Up @@ -302,8 +312,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 @@ -375,6 +388,12 @@ 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.Errorf("Misconfigured server: no Listener is configured.")
s.config.triggerServerStop()
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 All @@ -393,13 +412,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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not handle this misconfiguration the same as in Run?

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 this is misuse of API, so I want to handle it as a hard failure - not just error out and shutdown like normal server shutdown. The misconfig in Run is also misuse of API - if you are fine to make it fatal, I am happy to make the change.

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