github.com/phobos182/packer@v0.2.3-0.20130819023704-c84d2aeffc68/packer/plugin/client.go (about)

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