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