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