diff --git a/Documentation/usage/dlv_dap.md b/Documentation/usage/dlv_dap.md index 9ec39247d6..a929c6927c 100644 --- a/Documentation/usage/dlv_dap.md +++ b/Documentation/usage/dlv_dap.md @@ -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,7 +31,8 @@ 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 diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 51948e49f1..1885f96337 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -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.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 }() diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index 29ce4e7f59..7743f522da 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -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) diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index 5e078fc50e..86562515a1 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -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 diff --git a/service/dap/server.go b/service/dap/server.go index ffc41c334a..fe210e4259 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -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() - 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") @@ -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() @@ -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,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.