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