github.com/ezbercih/terraform@v0.1.1-0.20140729011846-3c33865e0839/plugin/client.go (about) 1 package plugin 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "net" 11 "net/rpc" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 "unicode" 19 ) 20 21 // If this is true, then the "unexpected EOF" panic will not be 22 // raised throughout the clients. 23 var Killed = false 24 25 // This is a slice of the "managed" clients which are cleaned up when 26 // calling Cleanup 27 var managedClients = make([]*Client, 0, 5) 28 29 // Client handles the lifecycle of a plugin application, determining its 30 // RPC address, and returning various types of Terraform interface implementations 31 // across the multi-process communication layer. 32 type Client struct { 33 config *ClientConfig 34 exited bool 35 doneLogging chan struct{} 36 l sync.Mutex 37 address net.Addr 38 service string 39 client *rpc.Client 40 } 41 42 // ClientConfig is the configuration used to initialize a new 43 // plugin client. After being used to initialize a plugin client, 44 // that configuration must not be modified again. 45 type ClientConfig struct { 46 // The unstarted subprocess for starting the plugin. 47 Cmd *exec.Cmd 48 49 // Managed represents if the client should be managed by the 50 // plugin package or not. If true, then by calling CleanupClients, 51 // it will automatically be cleaned up. Otherwise, the client 52 // user is fully responsible for making sure to Kill all plugin 53 // clients. By default the client is _not_ managed. 54 Managed bool 55 56 // The minimum and maximum port to use for communicating with 57 // the subprocess. If not set, this defaults to 10,000 and 25,000 58 // respectively. 59 MinPort, MaxPort uint 60 61 // StartTimeout is the timeout to wait for the plugin to say it 62 // has started successfully. 63 StartTimeout time.Duration 64 65 // If non-nil, then the stderr of the client will be written to here 66 // (as well as the log). 67 Stderr io.Writer 68 } 69 70 // This makes sure all the managed subprocesses are killed and properly 71 // logged. This should be called before the parent process running the 72 // plugins exits. 73 // 74 // This must only be called _once_. 75 func CleanupClients() { 76 // Set the killed to true so that we don't get unexpected panics 77 Killed = true 78 79 // Kill all the managed clients in parallel and use a WaitGroup 80 // to wait for them all to finish up. 81 var wg sync.WaitGroup 82 for _, client := range managedClients { 83 wg.Add(1) 84 85 go func(client *Client) { 86 client.Kill() 87 wg.Done() 88 }(client) 89 } 90 91 log.Println("waiting for all plugin processes to complete...") 92 wg.Wait() 93 } 94 95 // Creates a new plugin client which manages the lifecycle of an external 96 // plugin and gets the address for the RPC connection. 97 // 98 // The client must be cleaned up at some point by calling Kill(). If 99 // the client is a managed client (created with NewManagedClient) you 100 // can just call CleanupClients at the end of your program and they will 101 // be properly cleaned. 102 func NewClient(config *ClientConfig) (c *Client) { 103 if config.MinPort == 0 && config.MaxPort == 0 { 104 config.MinPort = 10000 105 config.MaxPort = 25000 106 } 107 108 if config.StartTimeout == 0 { 109 config.StartTimeout = 1 * time.Minute 110 } 111 112 if config.Stderr == nil { 113 config.Stderr = ioutil.Discard 114 } 115 116 c = &Client{config: config} 117 if config.Managed { 118 managedClients = append(managedClients, c) 119 } 120 121 return 122 } 123 124 // Client returns an RPC client for the plugin. 125 // 126 // Subsequent calls to this will return the same RPC client. 127 func (c *Client) Client() (*rpc.Client, error) { 128 addr, err := c.Start() 129 if err != nil { 130 return nil, err 131 } 132 133 c.l.Lock() 134 defer c.l.Unlock() 135 136 if c.client != nil { 137 return c.client, nil 138 } 139 140 conn, err := net.Dial(addr.Network(), addr.String()) 141 if err != nil { 142 return nil, err 143 } 144 145 if tcpConn, ok := conn.(*net.TCPConn); ok { 146 // Make sure to set keep alive so that the connection doesn't die 147 tcpConn.SetKeepAlive(true) 148 } 149 150 c.client = rpc.NewClient(conn) 151 return c.client, nil 152 } 153 154 // Tells whether or not the underlying process has exited. 155 func (c *Client) Exited() bool { 156 c.l.Lock() 157 defer c.l.Unlock() 158 return c.exited 159 } 160 161 // End the executing subprocess (if it is running) and perform any cleanup 162 // tasks necessary such as capturing any remaining logs and so on. 163 // 164 // This method blocks until the process successfully exits. 165 // 166 // This method can safely be called multiple times. 167 func (c *Client) Kill() { 168 cmd := c.config.Cmd 169 170 if cmd.Process == nil { 171 return 172 } 173 174 cmd.Process.Kill() 175 176 // Wait for the client to finish logging so we have a complete log 177 <-c.doneLogging 178 } 179 180 // Service returns the name of the service to use. 181 func (c *Client) Service() (string, error) { 182 if _, err := c.Start(); err != nil { 183 return "", err 184 } 185 186 return c.service, nil 187 } 188 189 // Starts the underlying subprocess, communicating with it to negotiate 190 // a port for RPC connections, and returning the address to connect via RPC. 191 // 192 // This method is safe to call multiple times. Subsequent calls have no effect. 193 // Once a client has been started once, it cannot be started again, even if 194 // it was killed. 195 func (c *Client) Start() (addr net.Addr, err error) { 196 c.l.Lock() 197 defer c.l.Unlock() 198 199 if c.address != nil { 200 return c.address, nil 201 } 202 203 c.doneLogging = make(chan struct{}) 204 205 env := []string{ 206 fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue), 207 fmt.Sprintf("TF_PLUGIN_MIN_PORT=%d", c.config.MinPort), 208 fmt.Sprintf("TF_PLUGIN_MAX_PORT=%d", c.config.MaxPort), 209 } 210 211 stdout_r, stdout_w := io.Pipe() 212 stderr_r, stderr_w := io.Pipe() 213 214 cmd := c.config.Cmd 215 cmd.Env = append(cmd.Env, os.Environ()...) 216 cmd.Env = append(cmd.Env, env...) 217 cmd.Stdin = os.Stdin 218 cmd.Stderr = stderr_w 219 cmd.Stdout = stdout_w 220 221 log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args) 222 err = cmd.Start() 223 if err != nil { 224 return 225 } 226 227 // Make sure the command is properly cleaned up if there is an error 228 defer func() { 229 r := recover() 230 231 if err != nil || r != nil { 232 cmd.Process.Kill() 233 } 234 235 if r != nil { 236 panic(r) 237 } 238 }() 239 240 // Start goroutine to wait for process to exit 241 exitCh := make(chan struct{}) 242 go func() { 243 // Make sure we close the write end of our stderr/stdout so 244 // that the readers send EOF properly. 245 defer stderr_w.Close() 246 defer stdout_w.Close() 247 248 // Wait for the command to end. 249 cmd.Wait() 250 251 // Log and make sure to flush the logs write away 252 log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path) 253 os.Stderr.Sync() 254 255 // Mark that we exited 256 close(exitCh) 257 258 // Set that we exited, which takes a lock 259 c.l.Lock() 260 defer c.l.Unlock() 261 c.exited = true 262 }() 263 264 // Start goroutine that logs the stderr 265 go c.logStderr(stderr_r) 266 267 // Start a goroutine that is going to be reading the lines 268 // out of stdout 269 linesCh := make(chan []byte) 270 go func() { 271 defer close(linesCh) 272 273 buf := bufio.NewReader(stdout_r) 274 for { 275 line, err := buf.ReadBytes('\n') 276 if line != nil { 277 linesCh <- line 278 } 279 280 if err == io.EOF { 281 return 282 } 283 } 284 }() 285 286 // Make sure after we exit we read the lines from stdout forever 287 // so they dont' block since it is an io.Pipe 288 defer func() { 289 go func() { 290 for _ = range linesCh { 291 } 292 }() 293 }() 294 295 // Some channels for the next step 296 timeout := time.After(c.config.StartTimeout) 297 298 // Start looking for the address 299 log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path) 300 select { 301 case <-timeout: 302 err = errors.New("timeout while waiting for plugin to start") 303 case <-exitCh: 304 err = errors.New("plugin exited before we could connect") 305 case lineBytes := <-linesCh: 306 // Trim the line and split by "|" in order to get the parts of 307 // the output. 308 line := strings.TrimSpace(string(lineBytes)) 309 parts := strings.SplitN(line, "|", 4) 310 if len(parts) < 4 { 311 err = fmt.Errorf("Unrecognized remote plugin message: %s", line) 312 return 313 } 314 315 // Test the API version 316 if parts[0] != APIVersion { 317 err = fmt.Errorf("Incompatible API version with plugin. "+ 318 "Plugin version: %s, Ours: %s", parts[0], APIVersion) 319 return 320 } 321 322 switch parts[1] { 323 case "tcp": 324 addr, err = net.ResolveTCPAddr("tcp", parts[2]) 325 case "unix": 326 addr, err = net.ResolveUnixAddr("unix", parts[2]) 327 default: 328 err = fmt.Errorf("Unknown address type: %s", parts[1]) 329 } 330 331 // Grab the services 332 c.service = parts[3] 333 } 334 335 c.address = addr 336 return 337 } 338 339 func (c *Client) logStderr(r io.Reader) { 340 bufR := bufio.NewReader(r) 341 for { 342 line, err := bufR.ReadString('\n') 343 if line != "" { 344 c.config.Stderr.Write([]byte(line)) 345 346 line = strings.TrimRightFunc(line, unicode.IsSpace) 347 log.Printf("%s: %s", filepath.Base(c.config.Cmd.Path), line) 348 } 349 350 if err == io.EOF { 351 break 352 } 353 } 354 355 // Flag that we've completed logging for others 356 close(c.doneLogging) 357 }