github.com/hashicorp/go-plugin@v1.6.0/rpc_client.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package plugin
     5  
     6  import (
     7  	"crypto/tls"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/rpc"
    12  
    13  	"github.com/hashicorp/yamux"
    14  )
    15  
    16  // RPCClient connects to an RPCServer over net/rpc to dispense plugin types.
    17  type RPCClient struct {
    18  	broker  *MuxBroker
    19  	control *rpc.Client
    20  	plugins map[string]Plugin
    21  
    22  	// These are the streams used for the various stdout/err overrides
    23  	stdout, stderr net.Conn
    24  }
    25  
    26  // newRPCClient creates a new RPCClient. The Client argument is expected
    27  // to be successfully started already with a lock held.
    28  func newRPCClient(c *Client) (*RPCClient, error) {
    29  	// Connect to the client
    30  	conn, err := net.Dial(c.address.Network(), c.address.String())
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	if tcpConn, ok := conn.(*net.TCPConn); ok {
    35  		// Make sure to set keep alive so that the connection doesn't die
    36  		tcpConn.SetKeepAlive(true)
    37  	}
    38  
    39  	if c.config.TLSConfig != nil {
    40  		conn = tls.Client(conn, c.config.TLSConfig)
    41  	}
    42  
    43  	// Create the actual RPC client
    44  	result, err := NewRPCClient(conn, c.config.Plugins)
    45  	if err != nil {
    46  		conn.Close()
    47  		return nil, err
    48  	}
    49  
    50  	// Begin the stream syncing so that stdin, out, err work properly
    51  	err = result.SyncStreams(
    52  		c.config.SyncStdout,
    53  		c.config.SyncStderr)
    54  	if err != nil {
    55  		result.Close()
    56  		return nil, err
    57  	}
    58  
    59  	return result, nil
    60  }
    61  
    62  // NewRPCClient creates a client from an already-open connection-like value.
    63  // Dial is typically used instead.
    64  func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClient, error) {
    65  	// Create the yamux client so we can multiplex
    66  	mux, err := yamux.Client(conn, nil)
    67  	if err != nil {
    68  		conn.Close()
    69  		return nil, err
    70  	}
    71  
    72  	// Connect to the control stream.
    73  	control, err := mux.Open()
    74  	if err != nil {
    75  		mux.Close()
    76  		return nil, err
    77  	}
    78  
    79  	// Connect stdout, stderr streams
    80  	stdstream := make([]net.Conn, 2)
    81  	for i, _ := range stdstream {
    82  		stdstream[i], err = mux.Open()
    83  		if err != nil {
    84  			mux.Close()
    85  			return nil, err
    86  		}
    87  	}
    88  
    89  	// Create the broker and start it up
    90  	broker := newMuxBroker(mux)
    91  	go broker.Run()
    92  
    93  	// Build the client using our broker and control channel.
    94  	return &RPCClient{
    95  		broker:  broker,
    96  		control: rpc.NewClient(control),
    97  		plugins: plugins,
    98  		stdout:  stdstream[0],
    99  		stderr:  stdstream[1],
   100  	}, nil
   101  }
   102  
   103  // SyncStreams should be called to enable syncing of stdout,
   104  // stderr with the plugin.
   105  //
   106  // This will return immediately and the syncing will continue to happen
   107  // in the background. You do not need to launch this in a goroutine itself.
   108  //
   109  // This should never be called multiple times.
   110  func (c *RPCClient) SyncStreams(stdout io.Writer, stderr io.Writer) error {
   111  	go copyStream("stdout", stdout, c.stdout)
   112  	go copyStream("stderr", stderr, c.stderr)
   113  	return nil
   114  }
   115  
   116  // Close closes the connection. The client is no longer usable after this
   117  // is called.
   118  func (c *RPCClient) Close() error {
   119  	// Call the control channel and ask it to gracefully exit. If this
   120  	// errors, then we save it so that we always return an error but we
   121  	// want to try to close the other channels anyways.
   122  	var empty struct{}
   123  	returnErr := c.control.Call("Control.Quit", true, &empty)
   124  
   125  	// Close the other streams we have
   126  	if err := c.control.Close(); err != nil {
   127  		return err
   128  	}
   129  	if err := c.stdout.Close(); err != nil {
   130  		return err
   131  	}
   132  	if err := c.stderr.Close(); err != nil {
   133  		return err
   134  	}
   135  	if err := c.broker.Close(); err != nil {
   136  		return err
   137  	}
   138  
   139  	// Return back the error we got from Control.Quit. This is very important
   140  	// since we MUST return non-nil error if this fails so that Client.Kill
   141  	// will properly try a process.Kill.
   142  	return returnErr
   143  }
   144  
   145  func (c *RPCClient) Dispense(name string) (interface{}, error) {
   146  	p, ok := c.plugins[name]
   147  	if !ok {
   148  		return nil, fmt.Errorf("unknown plugin type: %s", name)
   149  	}
   150  
   151  	var id uint32
   152  	if err := c.control.Call(
   153  		"Dispenser.Dispense", name, &id); err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	conn, err := c.broker.Dial(id)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	return p.Client(c.broker, rpc.NewClient(conn))
   163  }
   164  
   165  // Ping pings the connection to ensure it is still alive.
   166  //
   167  // The error from the RPC call is returned exactly if you want to inspect
   168  // it for further error analysis. Any error returned from here would indicate
   169  // that the connection to the plugin is not healthy.
   170  func (c *RPCClient) Ping() error {
   171  	var empty struct{}
   172  	return c.control.Call("Control.Ping", true, &empty)
   173  }