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