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