Skip to content

Commit

Permalink
cmds: add a new dap-reverse command
Browse files Browse the repository at this point in the history
This command is a helper command to be used by dlv dap when
launch request with a console property (integrated, external)
is received. The dlv dap server then asks the client to run
this command in the integrated or external terminal using the
RunInTerminal request and turns itself into a proxy mode that
forwards messages between the client and the dap-reverse command
run by the editor.

The dap-reverse command is similar to the dap command, except
that instead of opening a port and running as a server listening
on the port, this command dials to the supplied address (the
rendezvous port setup by the dlv dap server operating in proxy
mode). Once the dlv-reverse command is connected, the dlv dap
server will forward all the messages from the client (including
the initialize request and the launch request) and relay all
the responses from the dlv-reverse back to the client.

This command is internal use only, so it's intentionally hidden
from users - dlv usage and manual will not display info about
this.

Update golang/vscode-go#124
  • Loading branch information
hyangah committed Jul 3, 2021
1 parent 9dfd164 commit 5542532
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 2 deletions.
52 changes: 52 additions & 0 deletions cmd/dlv/cmds/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ execution is resumed at the start of the debug session.`,
}
rootCommand.AddCommand(dapCommand)

// 'dap-reverse' subcommand - this is for internal use only.
dapReverseCommand := &cobra.Command{
Use: "dap-reverse [host]:port",
Short: "A helper command the dlv dap server uses to launch a debugger in integrated/external console.",
Long: `A helper command the dlv dap server uses to launch a debugger in integrated/external console.
Given a launch request with the console attribute, the DAP server may need to start
the debugger/debuggee in the integrated or external console rather than setting up
an in-process debugger as usual. This command is a helper command that starts the
debugger process and connects to the provided host:port where the DAP server is
waiting. Once the connection is established, the DAP server runs as a proxy between
the editor and this external debugger process.`,
Run: dapReverseCmd,
Hidden: true, // This is for internal only.
}
rootCommand.AddCommand(dapReverseCommand)

// 'debug' subcommand.
debugCommand := &cobra.Command{
Use: "debug [package]",
Expand Down Expand Up @@ -462,6 +479,41 @@ func dapCmd(cmd *cobra.Command, args []string) {
os.Exit(status)
}

func dapReverseCmd(cmd *cobra.Command, args []string) {
status := func() int {
if err := logflags.Setup(log, logOutput, logDest); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return 1
}
defer logflags.Close()

if len(args) != 1 {
fmt.Fprintf(os.Stderr, "dap-reverse command requires [host]:port as the argument")
}
hostPort := args[0] // host:port where the dap server in proxy mode is waiting.
conn, err := net.Dial("tcp", hostPort)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to the DAP proxy server: %v\n", err)
return 1
}
disconnectChan := make(chan struct{})
server := dap.NewReverseDAPServer(&service.Config{
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
Backend: backend,
Foreground: tty == "",
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
},
}, conn)
defer server.Stop()
waitForDisconnectSignal(disconnectChan)
return 0
}()
os.Exit(status)
}

func debugCmd(cmd *cobra.Command, args []string) {
status := func() int {
debugname, err := filepath.Abs(cmd.Flag("output").Value.String())
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-reverse", "--log-output=dap", "--log", listener.Addr().String())
buf := &bytes.Buffer{}
cmd.Stdin = buf
cmd.Stdout = buf
assertNoError(cmd.Start(), t, "start dap-reverse 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 {
c := &Client{conn: conn, reader: bufio.NewReader(conn)}
c.seq = 1 // match VS Code numbering
return c
Expand Down
41 changes: 39 additions & 2 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:
//
// (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,6 +88,12 @@ 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 NewReverseDAPServer is a special
// DAP server, that is bound to a single net.Conn. Once the connection
// is closed, the server is stopped. Its Run is never called, but
// the NewReverseDAPServer immediately starts serveDAPCodec on the
// net.Conn.
type Server struct {
// config is all the information necessary to start the debugger and server.
config *service.Config
Expand Down Expand Up @@ -212,6 +219,33 @@ func NewServer(config *service.Config) *Server {
}
}

// NewReverseDAPServer returns a new DAP Server instance that is bound to
// the given net.Conn.
func NewReverseDAPServer(config *service.Config, conn net.Conn) *Server {
if config.Listener != nil {
panic("NewReverseDAPServer must be called without Listener")
}

// TODO(hyangah): separate Server (bound to a net.Listenr)
// and Session (bound to a net.Conn). NewReverseDAPServer
// is not actually a Server - it's not listening on a port,
// so the use of "Server" is not quite right.
logger := logflags.DAPLogger()
logger.Debug("Reverse DAP server pid = ", os.Getpid())
s := &Server{
conn: conn,
config: config,
stopTriggered: make(chan struct{}),
log: logger,
stackFrameHandles: newHandlesMap(),
variableHandles: newVariablesHandlesMap(),
args: defaultArgs,
exceptionErr: nil,
}
go s.serveDAPCodec()
return s
}

// If user-specified options are provided via Launch/AttachRequest,
// we override the defaults for optional args.
func (s *Server) setLaunchAttachArgs(request dap.LaunchAttachRequest) error {
Expand Down Expand Up @@ -265,7 +299,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

0 comments on commit 5542532

Please sign in to comment.