func (c *Client) Protocol() Protocol {
	_, err := c.Start()
	if err != nil {
		return ProtocolInvalid
	}
	return c.protocol
}
Protocol returns the protocol of server on the remote end. This will
start the plugin process if it isn't already started. Errors from
starting the plugin are surpressed and ProtocolInvalid is returned. It
is recommended you call Start explicitly before calling Protocol to ensure
no errors occur.
package plugin
import (
	"bufio"
	"context"
	"crypto/subtle"
	"crypto/tls"
	"errors"
	"fmt"
	"hash"
	"io"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"unicode"
	hclog "github.com/hashicorp/go-hclog"
)
var Killed uint32 = 0
var managedClients = make([]*Client, 0, 5)
var managedClientsLock sync.Mutex
var (
	ErrProcessNotFound = errors.New("Reattachment process not found")
	ErrChecksumsDoNotMatch = errors.New("checksums did not match")
	ErrSecureConfigNoChecksum = errors.New("no checksum provided")
	ErrSecureConfigNoHash = errors.New("no hash implementation provided")
	ErrSecureConfigAndReattach = errors.New("only one of Reattach or SecureConfig can be set")
)
type Client struct {
	config      *ClientConfig
	exited      bool
	doneLogging chan struct{}
	l           sync.Mutex
	address     net.Addr
	process     *os.Process
	client      ClientProtocol
	protocol    Protocol
	logger      hclog.Logger
	doneCtx     context.Context
}
type ClientConfig struct {
	HandshakeConfig
	Plugins map[string]Plugin
	Cmd      *exec.Cmd
	Reattach *ReattachConfig
	SecureConfig *SecureConfig
	TLSConfig *tls.Config
	Managed bool
	MinPort, MaxPort uint
	StartTimeout time.Duration
	Stderr io.Writer
	SyncStdout io.Writer
	SyncStderr io.Writer
	AllowedProtocols []Protocol
	Logger hclog.Logger
}
type ReattachConfig struct {
	Protocol Protocol
	Addr     net.Addr
	Pid      int
}
type SecureConfig struct {
	Checksum []byte
	Hash     hash.Hash
}
func (s *SecureConfig) Check(filePath string) (bool, error) {
	if len(s.Checksum) == 0 {
		return false, ErrSecureConfigNoChecksum
	}
	if s.Hash == nil {
		return false, ErrSecureConfigNoHash
	}
	file, err := os.Open(filePath)
	if err != nil {
		return false, err
	}
	defer file.Close()
	_, err = io.Copy(s.Hash, file)
	if err != nil {
		return false, err
	}
	sum := s.Hash.Sum(nil)
	return subtle.ConstantTimeCompare(sum, s.Checksum) == 1, nil
}
func CleanupClients() {
	atomic.StoreUint32(&Killed, 1)
	var wg sync.WaitGroup
	managedClientsLock.Lock()
	for _, client := range managedClients {
		wg.Add(1)
		go func(client *Client) {
			client.Kill()
			wg.Done()
		}(client)
	}
	managedClientsLock.Unlock()
	wg.Wait()
}
func NewClient(config *ClientConfig) (c *Client) {
	if config.MinPort == 0 && config.MaxPort == 0 {
		config.MinPort = 10000
		config.MaxPort = 25000
	}
	if config.StartTimeout == 0 {
		config.StartTimeout = 1 * time.Minute
	}
	if config.Stderr == nil {
		config.Stderr = ioutil.Discard
	}
	if config.SyncStdout == nil {
		config.SyncStdout = ioutil.Discard
	}
	if config.SyncStderr == nil {
		config.SyncStderr = ioutil.Discard
	}
	if config.AllowedProtocols == nil {
		config.AllowedProtocols = []Protocol{ProtocolNetRPC}
	}
	if config.Logger == nil {
		config.Logger = hclog.New(&hclog.LoggerOptions{
			Output: hclog.DefaultOutput,
			Level:  hclog.Trace,
			Name:   "plugin",
		})
	}
	c = &Client{
		config: config,
		logger: config.Logger,
	}
	if config.Managed {
		managedClientsLock.Lock()
		managedClients = append(managedClients, c)
		managedClientsLock.Unlock()
	}
	return
}
func (c *Client) Client() (ClientProtocol, error) {
	_, err := c.Start()
	if err != nil {
		return nil, err
	}
	c.l.Lock()
	defer c.l.Unlock()
	if c.client != nil {
		return c.client, nil
	}
	switch c.protocol {
	case ProtocolNetRPC:
		c.client, err = newRPCClient(c)
	case ProtocolGRPC:
		c.client, err = newGRPCClient(c.doneCtx, c)
	default:
		return nil, fmt.Errorf("unknown server protocol: %s", c.protocol)
	}
	if err != nil {
		c.client = nil
		return nil, err
	}
	return c.client, nil
}
func (c *Client) Exited() bool {
	c.l.Lock()
	defer c.l.Unlock()
	return c.exited
}
func (c *Client) Kill() {
	c.l.Lock()
	process := c.process
	addr := c.address
	doneCh := c.doneLogging
	c.l.Unlock()
	if process == nil {
		return
	}
	graceful := false
	if addr != nil {
		client, err := c.Client()
		if err == nil {
			err = client.Close()
			graceful = err == nil
			if err != nil {
				c.logger.Warn("error closing client during Kill", "err", err)
			}
		}
	}
	if graceful {
		select {
		case <-doneCh:
			return
		case <-time.After(250 * time.Millisecond):
		}
	}
	process.Kill()
	<-doneCh
}
func (c *Client) Start() (addr net.Addr, err error) {
	c.l.Lock()
	defer c.l.Unlock()
	if c.address != nil {
		return c.address, nil
	}
	{
		cmdSet := c.config.Cmd != nil
		attachSet := c.config.Reattach != nil
		secureSet := c.config.SecureConfig != nil
		if cmdSet == attachSet {
			return nil, fmt.Errorf("Only one of Cmd or Reattach must be set")
		}
		if secureSet && attachSet {
			return nil, ErrSecureConfigAndReattach
		}
	}
	c.doneLogging = make(chan struct{})
	var ctxCancel context.CancelFunc
	c.doneCtx, ctxCancel = context.WithCancel(context.Background())
	if c.config.Reattach != nil {
		p, err := os.FindProcess(c.config.Reattach.Pid)
		if err != nil {
			return nil, err
		}
		conn, err := net.Dial(
			c.config.Reattach.Addr.Network(),
			c.config.Reattach.Addr.String())
		if err != nil {
			p.Kill()
			return nil, ErrProcessNotFound
		}
		conn.Close()
		go func(pid int) {
			pidWait(pid)
			c.logger.Debug("reattached plugin process exited")
			c.l.Lock()
			defer c.l.Unlock()
			c.exited = true
			close(c.doneLogging)
			ctxCancel()
		}(p.Pid)
		c.address = c.config.Reattach.Addr
		c.process = p
		c.protocol = c.config.Reattach.Protocol
		if c.protocol == "" {
			c.protocol = ProtocolNetRPC
		}
		return c.address, nil
	}
	env := []string{
		fmt.Sprintf("%s=%s", c.config.MagicCookieKey, c.config.MagicCookieValue),
		fmt.Sprintf("PLUGIN_MIN_PORT=%d", c.config.MinPort),
		fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort),
	}
	stdout_r, stdout_w := io.Pipe()
	stderr_r, stderr_w := io.Pipe()
	cmd := c.config.Cmd
	cmd.Env = append(cmd.Env, os.Environ()...)
	cmd.Env = append(cmd.Env, env...)
	cmd.Stdin = os.Stdin
	cmd.Stderr = stderr_w
	cmd.Stdout = stdout_w
	if c.config.SecureConfig != nil {
		if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil {
			return nil, fmt.Errorf("error verifying checksum: %s", err)
		} else if !ok {
			return nil, ErrChecksumsDoNotMatch
		}
	}
	c.logger.Debug("starting plugin", "path", cmd.Path, "args", cmd.Args)
	err = cmd.Start()
	if err != nil {
		return
	}
	c.process = cmd.Process
	defer func() {
		r := recover()
		if err != nil || r != nil {
			cmd.Process.Kill()
		}
		if r != nil {
			panic(r)
		}
	}()
	exitCh := make(chan struct{})
	go func() {
		defer stderr_w.Close()
		defer stdout_w.Close()
		cmd.Wait()
		c.logger.Debug("plugin process exited", "path", cmd.Path)
		os.Stderr.Sync()
		close(exitCh)
		ctxCancel()
		c.l.Lock()
		defer c.l.Unlock()
		c.exited = true
	}()
	go c.logStderr(stderr_r)
	linesCh := make(chan []byte)
	go func() {
		defer close(linesCh)
		buf := bufio.NewReader(stdout_r)
		for {
			line, err := buf.ReadBytes('\n')
			if line != nil {
				linesCh <- line
			}
			if err == io.EOF {
				return
			}
		}
	}()
	defer func() {
		go func() {
			for _ = range linesCh {
			}
		}()
	}()
	timeout := time.After(c.config.StartTimeout)
	c.logger.Debug("waiting for RPC address", "path", cmd.Path)
	select {
	case <-timeout:
		err = errors.New("timeout while waiting for plugin to start")
	case <-exitCh:
		err = errors.New("plugin exited before we could connect")
	case lineBytes := <-linesCh:
		line := strings.TrimSpace(string(lineBytes))
		parts := strings.SplitN(line, "|", 6)
		if len(parts) < 4 {
			err = fmt.Errorf(
				"Unrecognized remote plugin message: %s\n\n"+
					"This usually means that the plugin is either invalid or simply\n"+
					"needs to be recompiled to support the latest protocol.", line)
			return
		}
		{
			var coreProtocol int64
			coreProtocol, err = strconv.ParseInt(parts[0], 10, 0)
			if err != nil {
				err = fmt.Errorf("Error parsing core protocol version: %s", err)
				return
			}
			if int(coreProtocol) != CoreProtocolVersion {
				err = fmt.Errorf("Incompatible core API version with plugin. "+
					"Plugin version: %s, Core version: %d\n\n"+
					"To fix this, the plugin usually only needs to be recompiled.\n"+
					"Please report this to the plugin author.", parts[0], CoreProtocolVersion)
				return
			}
		}
		var protocol int64
		protocol, err = strconv.ParseInt(parts[1], 10, 0)
		if err != nil {
			err = fmt.Errorf("Error parsing protocol version: %s", err)
			return
		}
		if uint(protocol) != c.config.ProtocolVersion {
			err = fmt.Errorf("Incompatible API version with plugin. "+
				"Plugin version: %s, Core version: %d", parts[1], c.config.ProtocolVersion)
			return
		}
		switch parts[2] {
		case "tcp":
			addr, err = net.ResolveTCPAddr("tcp", parts[3])
		case "unix":
			addr, err = net.ResolveUnixAddr("unix", parts[3])
		default:
			err = fmt.Errorf("Unknown address type: %s", parts[3])
		}
		c.protocol = ProtocolNetRPC
		if len(parts) >= 5 {
			c.protocol = Protocol(parts[4])
		}
		found := false
		for _, p := range c.config.AllowedProtocols {
			if p == c.protocol {
				found = true
				break
			}
		}
		if !found {
			err = fmt.Errorf("Unsupported plugin protocol %q. Supported: %v",
				c.protocol, c.config.AllowedProtocols)
			return
		}
	}
	c.address = addr
	return
}
func (c *Client) ReattachConfig() *ReattachConfig {
	c.l.Lock()
	defer c.l.Unlock()
	if c.address == nil {
		return nil
	}
	if c.config.Cmd != nil && c.config.Cmd.Process == nil {
		return nil
	}
	if c.config.Reattach != nil {
		return c.config.Reattach
	}
	return &ReattachConfig{
		Protocol: c.protocol,
		Addr:     c.address,
		Pid:      c.config.Cmd.Process.Pid,
	}
}