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  }