github.com/leowmjw/otto@v0.2.1-0.20160126165905-6400716cf085/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  	pluginrpc "github.com/hashicorp/otto/rpc"
    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 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  	client      *pluginrpc.Client
    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). This is the original os.Stderr of the subprocess.
    67  	// This isn't the output of synced stderr.
    68  	Stderr io.Writer
    69  
    70  	// SyncStdout, SyncStderr can be set to override the
    71  	// respective os.Std* values in the plugin. Care should be taken to
    72  	// avoid races here. If these are nil, then this will automatically be
    73  	// hooked up to os.Stdin, Stdout, and Stderr, respectively.
    74  	//
    75  	// If the default values (nil) are used, then this package will not
    76  	// sync any of these streams.
    77  	SyncStdout io.Writer
    78  	SyncStderr io.Writer
    79  }
    80  
    81  // This makes sure all the managed subprocesses are killed and properly
    82  // logged. This should be called before the parent process running the
    83  // plugins exits.
    84  //
    85  // This must only be called _once_.
    86  func CleanupClients() {
    87  	// Set the killed to true so that we don't get unexpected panics
    88  	Killed = true
    89  
    90  	// Kill all the managed clients in parallel and use a WaitGroup
    91  	// to wait for them all to finish up.
    92  	var wg sync.WaitGroup
    93  	for _, client := range managedClients {
    94  		wg.Add(1)
    95  
    96  		go func(client *Client) {
    97  			client.Kill()
    98  			wg.Done()
    99  		}(client)
   100  	}
   101  
   102  	log.Println("[DEBUG] waiting for all plugin processes to complete...")
   103  	wg.Wait()
   104  }
   105  
   106  // Creates a new plugin client which manages the lifecycle of an external
   107  // plugin and gets the address for the RPC connection.
   108  //
   109  // The client must be cleaned up at some point by calling Kill(). If
   110  // the client is a managed client (created with NewManagedClient) you
   111  // can just call CleanupClients at the end of your program and they will
   112  // be properly cleaned.
   113  func NewClient(config *ClientConfig) (c *Client) {
   114  	if config.MinPort == 0 && config.MaxPort == 0 {
   115  		config.MinPort = 10000
   116  		config.MaxPort = 25000
   117  	}
   118  
   119  	if config.StartTimeout == 0 {
   120  		config.StartTimeout = 1 * time.Minute
   121  	}
   122  
   123  	if config.Stderr == nil {
   124  		config.Stderr = ioutil.Discard
   125  	}
   126  
   127  	if config.SyncStdout == nil {
   128  		config.SyncStdout = ioutil.Discard
   129  	}
   130  	if config.SyncStderr == nil {
   131  		config.SyncStderr = ioutil.Discard
   132  	}
   133  
   134  	c = &Client{config: config}
   135  	if config.Managed {
   136  		managedClients = append(managedClients, c)
   137  	}
   138  
   139  	return
   140  }
   141  
   142  // Client returns an RPC client for the plugin.
   143  //
   144  // Subsequent calls to this will return the same RPC client.
   145  func (c *Client) Client() (*pluginrpc.Client, error) {
   146  	addr, err := c.Start()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	c.l.Lock()
   152  	defer c.l.Unlock()
   153  
   154  	if c.client != nil {
   155  		return c.client, nil
   156  	}
   157  
   158  	c.client, err = pluginrpc.Dial(addr.Network(), addr.String())
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// Begin the stream syncing so that stdin, out, err work properly
   164  	err = c.client.SyncStreams(
   165  		c.config.SyncStdout,
   166  		c.config.SyncStderr)
   167  	if err != nil {
   168  		c.client.Close()
   169  		c.client = nil
   170  		return nil, err
   171  	}
   172  
   173  	return c.client, nil
   174  }
   175  
   176  // Tells whether or not the underlying process has exited.
   177  func (c *Client) Exited() bool {
   178  	c.l.Lock()
   179  	defer c.l.Unlock()
   180  	return c.exited
   181  }
   182  
   183  // End the executing subprocess (if it is running) and perform any cleanup
   184  // tasks necessary such as capturing any remaining logs and so on.
   185  //
   186  // This method blocks until the process successfully exits.
   187  //
   188  // This method can safely be called multiple times.
   189  func (c *Client) Kill() {
   190  	cmd := c.config.Cmd
   191  
   192  	if cmd.Process == nil {
   193  		return
   194  	}
   195  
   196  	cmd.Process.Kill()
   197  
   198  	// Wait for the client to finish logging so we have a complete log
   199  	<-c.doneLogging
   200  }
   201  
   202  // Starts the underlying subprocess, communicating with it to negotiate
   203  // a port for RPC connections, and returning the address to connect via RPC.
   204  //
   205  // This method is safe to call multiple times. Subsequent calls have no effect.
   206  // Once a client has been started once, it cannot be started again, even if
   207  // it was killed.
   208  func (c *Client) Start() (addr net.Addr, err error) {
   209  	c.l.Lock()
   210  	defer c.l.Unlock()
   211  
   212  	if c.address != nil {
   213  		return c.address, nil
   214  	}
   215  
   216  	c.doneLogging = make(chan struct{})
   217  
   218  	env := []string{
   219  		fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
   220  		fmt.Sprintf("OTTO_PLUGIN_MIN_PORT=%d", c.config.MinPort),
   221  		fmt.Sprintf("OTTO_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
   222  	}
   223  
   224  	stdout_r, stdout_w := io.Pipe()
   225  	stderr_r, stderr_w := io.Pipe()
   226  
   227  	cmd := c.config.Cmd
   228  	cmd.Env = append(cmd.Env, os.Environ()...)
   229  	cmd.Env = append(cmd.Env, env...)
   230  	cmd.Stdin = os.Stdin
   231  	cmd.Stderr = stderr_w
   232  	cmd.Stdout = stdout_w
   233  
   234  	log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args)
   235  	err = cmd.Start()
   236  	if err != nil {
   237  		return
   238  	}
   239  
   240  	// Make sure the command is properly cleaned up if there is an error
   241  	defer func() {
   242  		r := recover()
   243  
   244  		if err != nil || r != nil {
   245  			cmd.Process.Kill()
   246  		}
   247  
   248  		if r != nil {
   249  			panic(r)
   250  		}
   251  	}()
   252  
   253  	// Start goroutine to wait for process to exit
   254  	exitCh := make(chan struct{})
   255  	go func() {
   256  		// Make sure we close the write end of our stderr/stdout so
   257  		// that the readers send EOF properly.
   258  		defer stderr_w.Close()
   259  		defer stdout_w.Close()
   260  
   261  		// Wait for the command to end.
   262  		cmd.Wait()
   263  
   264  		// Log and make sure to flush the logs write away
   265  		log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path)
   266  		os.Stderr.Sync()
   267  
   268  		// Mark that we exited
   269  		close(exitCh)
   270  
   271  		// Set that we exited, which takes a lock
   272  		c.l.Lock()
   273  		defer c.l.Unlock()
   274  		c.exited = true
   275  	}()
   276  
   277  	// Start goroutine that logs the stderr
   278  	go c.logStderr(stderr_r)
   279  
   280  	// Start a goroutine that is going to be reading the lines
   281  	// out of stdout
   282  	linesCh := make(chan []byte)
   283  	go func() {
   284  		defer close(linesCh)
   285  
   286  		buf := bufio.NewReader(stdout_r)
   287  		for {
   288  			line, err := buf.ReadBytes('\n')
   289  			if line != nil {
   290  				linesCh <- line
   291  			}
   292  
   293  			if err == io.EOF {
   294  				return
   295  			}
   296  		}
   297  	}()
   298  
   299  	// Make sure after we exit we read the lines from stdout forever
   300  	// so they don't block since it is an io.Pipe
   301  	defer func() {
   302  		go func() {
   303  			for _ = range linesCh {
   304  			}
   305  		}()
   306  	}()
   307  
   308  	// Some channels for the next step
   309  	timeout := time.After(c.config.StartTimeout)
   310  
   311  	// Start looking for the address
   312  	log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path)
   313  	select {
   314  	case <-timeout:
   315  		err = errors.New("timeout while waiting for plugin to start")
   316  	case <-exitCh:
   317  		err = errors.New("plugin exited before we could connect")
   318  	case lineBytes := <-linesCh:
   319  		// Trim the line and split by "|" in order to get the parts of
   320  		// the output.
   321  		line := strings.TrimSpace(string(lineBytes))
   322  		parts := strings.SplitN(line, "|", 3)
   323  		if len(parts) < 3 {
   324  			err = fmt.Errorf("Unrecognized remote plugin message: %s", line)
   325  			return
   326  		}
   327  
   328  		// Test the API version
   329  		if parts[0] != APIVersion {
   330  			err = fmt.Errorf("Incompatible API version with plugin. "+
   331  				"Plugin version: %s, Ours: %s", parts[0], APIVersion)
   332  			return
   333  		}
   334  
   335  		switch parts[1] {
   336  		case "tcp":
   337  			addr, err = net.ResolveTCPAddr("tcp", parts[2])
   338  		case "unix":
   339  			addr, err = net.ResolveUnixAddr("unix", parts[2])
   340  		default:
   341  			err = fmt.Errorf("Unknown address type: %s", parts[1])
   342  		}
   343  	}
   344  
   345  	c.address = addr
   346  	return
   347  }
   348  
   349  func (c *Client) logStderr(r io.Reader) {
   350  	bufR := bufio.NewReader(r)
   351  	for {
   352  		line, err := bufR.ReadString('\n')
   353  		if line != "" {
   354  			c.config.Stderr.Write([]byte(line))
   355  
   356  			line = strings.TrimRightFunc(line, unicode.IsSpace)
   357  			log.Printf("[DEBUG] %s: %s", filepath.Base(c.config.Cmd.Path), line)
   358  		}
   359  
   360  		if err == io.EOF {
   361  			break
   362  		}
   363  	}
   364  
   365  	// Flag that we've completed logging for others
   366  	close(c.doneLogging)
   367  }