github.com/yoctocloud/packer@v0.6.2-0.20160520224004-e11a0a18423f/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 hook implementation that is communicating over this 142 // client. If the client hasn't been started, this will start it. 143 func (c *Client) Hook() (packer.Hook, error) { 144 client, err := c.packrpcClient() 145 if err != nil { 146 return nil, err 147 } 148 149 return &cmdHook{client.Hook(), c}, nil 150 } 151 152 // Returns a post-processor implementation that is communicating over 153 // this client. If the client hasn't been started, this will start it. 154 func (c *Client) PostProcessor() (packer.PostProcessor, error) { 155 client, err := c.packrpcClient() 156 if err != nil { 157 return nil, err 158 } 159 160 return &cmdPostProcessor{client.PostProcessor(), c}, nil 161 } 162 163 // Returns a provisioner implementation that is communicating over this 164 // client. If the client hasn't been started, this will start it. 165 func (c *Client) Provisioner() (packer.Provisioner, error) { 166 client, err := c.packrpcClient() 167 if err != nil { 168 return nil, err 169 } 170 171 return &cmdProvisioner{client.Provisioner(), c}, nil 172 } 173 174 // End the executing subprocess (if it is running) and perform any cleanup 175 // tasks necessary such as capturing any remaining logs and so on. 176 // 177 // This method blocks until the process successfully exits. 178 // 179 // This method can safely be called multiple times. 180 func (c *Client) Kill() { 181 cmd := c.config.Cmd 182 183 if cmd.Process == nil { 184 return 185 } 186 187 cmd.Process.Kill() 188 189 // Wait for the client to finish logging so we have a complete log 190 <-c.doneLogging 191 } 192 193 // Starts the underlying subprocess, communicating with it to negotiate 194 // a port for RPC connections, and returning the address to connect via RPC. 195 // 196 // This method is safe to call multiple times. Subsequent calls have no effect. 197 // Once a client has been started once, it cannot be started again, even if 198 // it was killed. 199 func (c *Client) Start() (addr net.Addr, err error) { 200 c.l.Lock() 201 defer c.l.Unlock() 202 203 if c.address != nil { 204 return c.address, nil 205 } 206 207 c.doneLogging = make(chan struct{}) 208 209 env := []string{ 210 fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue), 211 fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort), 212 fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort), 213 } 214 215 stdout_r, stdout_w := io.Pipe() 216 stderr_r, stderr_w := io.Pipe() 217 218 cmd := c.config.Cmd 219 cmd.Env = append(cmd.Env, os.Environ()...) 220 cmd.Env = append(cmd.Env, env...) 221 cmd.Stdin = os.Stdin 222 cmd.Stderr = stderr_w 223 cmd.Stdout = stdout_w 224 225 log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args) 226 err = cmd.Start() 227 if err != nil { 228 return 229 } 230 231 // Make sure the command is properly cleaned up if there is an error 232 defer func() { 233 r := recover() 234 235 if err != nil || r != nil { 236 cmd.Process.Kill() 237 } 238 239 if r != nil { 240 panic(r) 241 } 242 }() 243 244 // Start goroutine to wait for process to exit 245 exitCh := make(chan struct{}) 246 go func() { 247 // Make sure we close the write end of our stderr/stdout so 248 // that the readers send EOF properly. 249 defer stderr_w.Close() 250 defer stdout_w.Close() 251 252 // Wait for the command to end. 253 cmd.Wait() 254 255 // Log and make sure to flush the logs write away 256 log.Printf("%s: plugin process exited\n", cmd.Path) 257 os.Stderr.Sync() 258 259 // Mark that we exited 260 close(exitCh) 261 262 // Set that we exited, which takes a lock 263 c.l.Lock() 264 defer c.l.Unlock() 265 c.exited = true 266 }() 267 268 // Start goroutine that logs the stderr 269 go c.logStderr(stderr_r) 270 271 // Start a goroutine that is going to be reading the lines 272 // out of stdout 273 linesCh := make(chan []byte) 274 go func() { 275 defer close(linesCh) 276 277 buf := bufio.NewReader(stdout_r) 278 for { 279 line, err := buf.ReadBytes('\n') 280 if line != nil { 281 linesCh <- line 282 } 283 284 if err == io.EOF { 285 return 286 } 287 } 288 }() 289 290 // Make sure after we exit we read the lines from stdout forever 291 // so they dont' block since it is an io.Pipe 292 defer func() { 293 go func() { 294 for _ = range linesCh { 295 } 296 }() 297 }() 298 299 // Some channels for the next step 300 timeout := time.After(c.config.StartTimeout) 301 302 // Start looking for the address 303 log.Printf("Waiting for RPC address for: %s", cmd.Path) 304 select { 305 case <-timeout: 306 err = errors.New("timeout while waiting for plugin to start") 307 case <-exitCh: 308 err = errors.New("plugin exited before we could connect") 309 case lineBytes := <-linesCh: 310 // Trim the line and split by "|" in order to get the parts of 311 // the output. 312 line := strings.TrimSpace(string(lineBytes)) 313 parts := strings.SplitN(line, "|", 3) 314 if len(parts) < 3 { 315 err = fmt.Errorf("Unrecognized remote plugin message: %s", line) 316 return 317 } 318 319 // Test the API version 320 if parts[0] != APIVersion { 321 err = fmt.Errorf("Incompatible API version with plugin. "+ 322 "Plugin version: %s, Ours: %s", parts[0], APIVersion) 323 return 324 } 325 326 switch parts[1] { 327 case "tcp": 328 addr, err = net.ResolveTCPAddr("tcp", parts[2]) 329 case "unix": 330 addr, err = net.ResolveUnixAddr("unix", parts[2]) 331 default: 332 err = fmt.Errorf("Unknown address type: %s", parts[1]) 333 } 334 } 335 336 c.address = addr 337 return 338 } 339 340 func (c *Client) logStderr(r io.Reader) { 341 bufR := bufio.NewReader(r) 342 for { 343 line, err := bufR.ReadString('\n') 344 if line != "" { 345 c.config.Stderr.Write([]byte(line)) 346 347 line = strings.TrimRightFunc(line, unicode.IsSpace) 348 log.Printf("%s: %s", filepath.Base(c.config.Cmd.Path), line) 349 } 350 351 if err == io.EOF { 352 break 353 } 354 } 355 356 // Flag that we've completed logging for others 357 close(c.doneLogging) 358 } 359 360 func (c *Client) packrpcClient() (*packrpc.Client, error) { 361 addr, err := c.Start() 362 if err != nil { 363 return nil, err 364 } 365 366 conn, err := net.Dial(addr.Network(), addr.String()) 367 if err != nil { 368 return nil, err 369 } 370 371 if tcpConn, ok := conn.(*net.TCPConn); ok { 372 // Make sure to set keep alive so that the connection doesn't die 373 tcpConn.SetKeepAlive(true) 374 } 375 376 client, err := packrpc.NewClient(conn) 377 if err != nil { 378 conn.Close() 379 return nil, err 380 } 381 382 return client, nil 383 }