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 }