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