diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index fc7572e4..9ff316cb 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -317,6 +317,13 @@ func traceCmd(cmd *cobra.Command, args []string) { return 1 } + if Headless { + fmt.Fprintf(os.Stderr, "Warning: headless mode not supported with trace\n") + } + if AcceptMulti { + fmt.Fprintf(os.Stderr, "Warning: accept multiclient mode not supported with trace") + } + debugname, err := filepath.Abs(cmd.Flag("output").Value.String()) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) @@ -343,12 +350,9 @@ func traceCmd(cmd *cobra.Command, args []string) { processArgs = append([]string{debugname}, targetArgs...) } - // Make a TCP listener - listener, err := net.Listen("tcp", Addr) - if err != nil { - fmt.Printf("couldn't start listener: %s\n", err) - return 1 - } + + // Make a local in-memory connection that client and server use to communicate + listener, clientConn := service.ListenerPipe() defer listener.Close() // Create and start a debug server @@ -364,7 +368,7 @@ func traceCmd(cmd *cobra.Command, args []string) { fmt.Fprintln(os.Stderr, err) return 1 } - client := rpc2.NewClient(listener.Addr().String()) + client := rpc2.NewClientFromConn(clientConn) funcs, err := client.ListFunctions(regexp) if err != nil { fmt.Fprintln(os.Stderr, err) @@ -430,7 +434,7 @@ func connectCmd(cmd *cobra.Command, args []string) { fmt.Fprint(os.Stderr, "An empty address was provided. You must provide an address as the first argument.\n") os.Exit(1) } - os.Exit(connect(addr, conf, executingOther)) + os.Exit(connect(addr, nil, conf, executingOther)) } func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) { @@ -440,9 +444,14 @@ func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) { return args, []string{} } -func connect(addr string, conf *config.Config, kind executeKind) int { +func connect(addr string, clientConn net.Conn, conf *config.Config, kind executeKind) int { // Create and start a terminal - attach to running instance - client := rpc2.NewClient(addr) + var client *rpc2.RPCClient + if clientConn != nil { + client = rpc2.NewClientFromConn(clientConn) + } else { + client = rpc2.NewClient(addr) + } if client.Recorded() && (kind == executingGeneratedFile || kind == executingGeneratedTest) { // When using the rr backend remove the trace directory if we built the // executable @@ -474,14 +483,6 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile return 1 } - // Make a TCP listener - listener, err := net.Listen("tcp", Addr) - if err != nil { - fmt.Printf("couldn't start listener: %s\n", err) - return 1 - } - defer listener.Close() - if Headless && (InitFile != "") { fmt.Fprint(os.Stderr, "Warning: init file ignored\n") } @@ -493,6 +494,22 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile AcceptMulti = false } + var listener net.Listener + var clientConn net.Conn + var err error + + // Make a TCP listener + if Headless { + listener, err = net.Listen("tcp", Addr) + } else { + listener, clientConn = service.ListenerPipe() + } + if err != nil { + fmt.Printf("couldn't start listener: %s\n", err) + return 1 + } + defer listener.Close() + var server interface { Run() error Stop() error @@ -554,7 +571,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile return status } - return connect(listener.Addr().String(), conf, kind) + return connect(listener.Addr().String(), clientConn, conf, kind) } func optflags(args []string) []string { diff --git a/service/listenerpipe.go b/service/listenerpipe.go new file mode 100644 index 00000000..952d492e --- /dev/null +++ b/service/listenerpipe.go @@ -0,0 +1,60 @@ +package service + +import ( + "errors" + "net" + "sync" +) + +// ListenerPipe returns a full-duplex in-memory connection, like net.Pipe. +// Unlike net.Pipe one end of the connection is returned as an object +// satisfying the net.Listener interface. +// The first call to the Accept method of this object will return a net.Conn +// connected to the other net.Conn returned by ListenerPipe. +// Any subsequent calls to Accept will block until the listener is closed. +func ListenerPipe() (net.Listener, net.Conn) { + conn0, conn1 := net.Pipe() + return &preconnectedListener{conn: conn0, closech: make(chan struct{})}, conn1 +} + +// preconnectedListener satisfies the net.Listener interface by accepting a +// single pre-established connection. +// The first call to Accept will return the conn field, any subsequent call +// will block until the listener is closed. +type preconnectedListener struct { + accepted bool + conn net.Conn + closech chan struct{} + closeMu sync.Mutex + acceptMu sync.Mutex +} + +// Accept returns the pre-established connection the first time it's called, +// it blocks until the listener is closed on every subsequent call. +func (l *preconnectedListener) Accept() (net.Conn, error) { + l.acceptMu.Lock() + defer l.acceptMu.Unlock() + if !l.accepted { + l.accepted = true + return l.conn, nil + } + <-l.closech + return nil, errors.New("accept failed: listener closed") +} + +// Close closes the listener. +func (l *preconnectedListener) Close() error { + l.closeMu.Lock() + defer l.closeMu.Unlock() + if l.closech == nil { + return nil + } + close(l.closech) + l.closech = nil + return nil +} + +// Addr returns the listener's network address. +func (l *preconnectedListener) Addr() net.Addr { + return l.conn.LocalAddr() +} diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 34415177..bb22cd2e 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -3,6 +3,7 @@ package rpc2 import ( "fmt" "log" + "net" "net/rpc" "net/rpc/jsonrpc" "time" @@ -13,7 +14,6 @@ import ( // Client is a RPC service.Client. type RPCClient struct { - addr string client *rpc.Client retValLoadCfg *api.LoadConfig @@ -28,11 +28,20 @@ func NewClient(addr string) *RPCClient { if err != nil { log.Fatal("dialing:", err) } - c := &RPCClient{addr: addr, client: client} + return newFromRPCClient(client) +} + +func newFromRPCClient(client *rpc.Client) *RPCClient { + c := &RPCClient{client: client} c.call("SetApiVersion", api.SetAPIVersionIn{2}, &api.SetAPIVersionOut{}) return c } +// NewClientFromConn creates a new RPCClient from the given connection. +func NewClientFromConn(conn net.Conn) *RPCClient { + return newFromRPCClient(jsonrpc.NewClient(conn)) +} + func (c *RPCClient) ProcessPid() int { out := new(ProcessPidOut) c.call("ProcessPid", ProcessPidIn{}, out) diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 17fc21be..4c49da79 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -41,11 +41,7 @@ func withTestClient2(name string, t *testing.T, fn func(c service.Client)) { if testBackend == "rr" { protest.MustHaveRecordingAllowed(t) } - listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - t.Fatalf("couldn't start listener: %s\n", err) - } - defer listener.Close() + listener, clientConn := service.ListenerPipe() server := rpccommon.NewServer(&service.Config{ Listener: listener, ProcessArgs: []string{protest.BuildFixture(name, 0).Path}, @@ -54,7 +50,7 @@ func withTestClient2(name string, t *testing.T, fn func(c service.Client)) { if err := server.Run(); err != nil { t.Fatal(err) } - client := rpc2.NewClient(listener.Addr().String()) + client := rpc2.NewClientFromConn(clientConn) defer func() { dir, _ := client.TraceDirectory() client.Detach(true)