github.com/atsaki/terraform@v0.4.3-0.20150919165407-25bba5967654/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 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "sync" 16 "time" 17 "unicode" 18 19 tfrpc "github.com/hashicorp/terraform/rpc" 20 ) 21 22 // If this is true, then the "unexpected EOF" panic will not be 23 // raised throughout the clients. 24 var Killed = false 25 26 // This is a slice of the "managed" clients which are cleaned up when 27 // calling Cleanup 28 var managedClients = make([]*Client, 0, 5) 29 30 // Client handles the lifecycle of a plugin application, determining its 31 // RPC address, and returning various types of Terraform interface implementations 32 // across the multi-process communication layer. 33 type Client struct { 34 config *ClientConfig 35 exited bool 36 doneLogging chan struct{} 37 l sync.Mutex 38 address net.Addr 39 client *tfrpc.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() (*tfrpc.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 c.client, err = tfrpc.Dial(addr.Network(), addr.String()) 141 if err != nil { 142 return nil, err 143 } 144 145 return c.client, nil 146 } 147 148 // Tells whether or not the underlying process has exited. 149 func (c *Client) Exited() bool { 150 c.l.Lock() 151 defer c.l.Unlock() 152 return c.exited 153 } 154 155 // End the executing subprocess (if it is running) and perform any cleanup 156 // tasks necessary such as capturing any remaining logs and so on. 157 // 158 // This method blocks until the process successfully exits. 159 // 160 // This method can safely be called multiple times. 161 func (c *Client) Kill() { 162 cmd := c.config.Cmd 163 164 if cmd.Process == nil { 165 return 166 } 167 168 cmd.Process.Kill() 169 170 // Wait for the client to finish logging so we have a complete log 171 <-c.doneLogging 172 } 173 174 // Starts the underlying subprocess, communicating with it to negotiate 175 // a port for RPC connections, and returning the address to connect via RPC. 176 // 177 // This method is safe to call multiple times. Subsequent calls have no effect. 178 // Once a client has been started once, it cannot be started again, even if 179 // it was killed. 180 func (c *Client) Start() (addr net.Addr, err error) { 181 c.l.Lock() 182 defer c.l.Unlock() 183 184 if c.address != nil { 185 return c.address, nil 186 } 187 188 c.doneLogging = make(chan struct{}) 189 190 env := []string{ 191 fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue), 192 fmt.Sprintf("TF_PLUGIN_MIN_PORT=%d", c.config.MinPort), 193 fmt.Sprintf("TF_PLUGIN_MAX_PORT=%d", c.config.MaxPort), 194 } 195 196 stdout_r, stdout_w := io.Pipe() 197 stderr_r, stderr_w := io.Pipe() 198 199 cmd := c.config.Cmd 200 cmd.Env = append(cmd.Env, os.Environ()...) 201 cmd.Env = append(cmd.Env, env...) 202 cmd.Stdin = os.Stdin 203 cmd.Stderr = stderr_w 204 cmd.Stdout = stdout_w 205 206 log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args) 207 err = cmd.Start() 208 if err != nil { 209 return 210 } 211 212 // Make sure the command is properly cleaned up if there is an error 213 defer func() { 214 r := recover() 215 216 if err != nil || r != nil { 217 cmd.Process.Kill() 218 } 219 220 if r != nil { 221 panic(r) 222 } 223 }() 224 225 // Start goroutine to wait for process to exit 226 exitCh := make(chan struct{}) 227 go func() { 228 // Make sure we close the write end of our stderr/stdout so 229 // that the readers send EOF properly. 230 defer stderr_w.Close() 231 defer stdout_w.Close() 232 233 // Wait for the command to end. 234 cmd.Wait() 235 236 // Log and make sure to flush the logs write away 237 log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path) 238 os.Stderr.Sync() 239 240 // Mark that we exited 241 close(exitCh) 242 243 // Set that we exited, which takes a lock 244 c.l.Lock() 245 defer c.l.Unlock() 246 c.exited = true 247 }() 248 249 // Start goroutine that logs the stderr 250 go c.logStderr(stderr_r) 251 252 // Start a goroutine that is going to be reading the lines 253 // out of stdout 254 linesCh := make(chan []byte) 255 go func() { 256 defer close(linesCh) 257 258 buf := bufio.NewReader(stdout_r) 259 for { 260 line, err := buf.ReadBytes('\n') 261 if line != nil { 262 linesCh <- line 263 } 264 265 if err == io.EOF { 266 return 267 } 268 } 269 }() 270 271 // Make sure after we exit we read the lines from stdout forever 272 // so they don't block since it is an io.Pipe 273 defer func() { 274 go func() { 275 for _ = range linesCh { 276 } 277 }() 278 }() 279 280 // Some channels for the next step 281 timeout := time.After(c.config.StartTimeout) 282 283 // Start looking for the address 284 log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path) 285 select { 286 case <-timeout: 287 err = errors.New("timeout while waiting for plugin to start") 288 case <-exitCh: 289 err = errors.New("plugin exited before we could connect") 290 case lineBytes := <-linesCh: 291 // Trim the line and split by "|" in order to get the parts of 292 // the output. 293 line := strings.TrimSpace(string(lineBytes)) 294 parts := strings.SplitN(line, "|", 3) 295 if len(parts) < 3 { 296 err = fmt.Errorf("Unrecognized remote plugin message: %s", line) 297 return 298 } 299 300 // Test the API version 301 if parts[0] != APIVersion { 302 err = fmt.Errorf("Incompatible API version with plugin. "+ 303 "Plugin version: %s, Ours: %s", parts[0], APIVersion) 304 return 305 } 306 307 switch parts[1] { 308 case "tcp": 309 addr, err = net.ResolveTCPAddr("tcp", parts[2]) 310 case "unix": 311 addr, err = net.ResolveUnixAddr("unix", parts[2]) 312 default: 313 err = fmt.Errorf("Unknown address type: %s", parts[1]) 314 } 315 } 316 317 c.address = addr 318 return 319 } 320 321 func (c *Client) logStderr(r io.Reader) { 322 bufR := bufio.NewReader(r) 323 for { 324 line, err := bufR.ReadString('\n') 325 if line != "" { 326 c.config.Stderr.Write([]byte(line)) 327 328 line = strings.TrimRightFunc(line, unicode.IsSpace) 329 log.Printf("%s: %s", filepath.Base(c.config.Cmd.Path), line) 330 } 331 332 if err == io.EOF { 333 break 334 } 335 } 336 337 // Flag that we've completed logging for others 338 close(c.doneLogging) 339 }