github.com/dahs81/otto@v0.2.1-0.20160126165905-6400716cf085/command/plugin_manager.go (about)

     1  package command
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"os/exec"
    10  	"runtime"
    11  	"sync"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  	"github.com/hashicorp/otto/app"
    15  	"github.com/hashicorp/otto/helper/semaphore"
    16  	"github.com/hashicorp/otto/otto"
    17  	"github.com/hashicorp/otto/plugin"
    18  	"github.com/kardianos/osext"
    19  )
    20  
    21  // PluginGlob is the glob pattern used to find plugins.
    22  const PluginGlob = "otto-plugin-*"
    23  
    24  // PluginManager is responsible for discovering and starting plugins.
    25  //
    26  // Plugin cleanup is done out in the main package: we just defer
    27  // plugin.CleanupClients in main itself.
    28  type PluginManager struct {
    29  	// PluginDirs are the directories where plugins can be found.
    30  	// Any plugins with the same types found later (higher index) will
    31  	// override earlier (lower index) directories.
    32  	PluginDirs []string
    33  
    34  	// PluginMap is the map of availabile built-in plugins
    35  	PluginMap plugin.ServeMuxMap
    36  
    37  	plugins []*Plugin
    38  }
    39  
    40  // Plugin is a single plugin that has been loaded.
    41  type Plugin struct {
    42  	// Path and Args are the method used to invocate this plugin.
    43  	// These are the only two values that need to be set manually. Once
    44  	// these are set, call Load to load the plugin.
    45  	Path string   `json:"path,omitempty"`
    46  	Args []string `json:"args"`
    47  
    48  	// Builtin will be set to true by the PluginManager if this plugin
    49  	// represents a built-in plugin. If it does, then Path above has
    50  	// no affect, we always use the current executable.
    51  	Builtin bool `json:"builtin"`
    52  
    53  	// The fields below are loaded as part of the Load() call and should
    54  	// not be set manually, but can be accessed after Load.
    55  	App     app.Factory `json:"-"`
    56  	AppMeta *app.Meta   `json:"-"`
    57  
    58  	used bool
    59  }
    60  
    61  // Load loads the plugin specified by the Path and instantiates the
    62  // other fields on this structure.
    63  func (p *Plugin) Load() error {
    64  	// If it is builtin, then we always use our own path
    65  	path := p.Path
    66  	if p.Builtin {
    67  		path = pluginExePath
    68  	}
    69  
    70  	// Create the plugin client to communicate with the process
    71  	pluginClient := plugin.NewClient(&plugin.ClientConfig{
    72  		Cmd:        exec.Command(path, p.Args...),
    73  		Managed:    true,
    74  		SyncStdout: os.Stdout,
    75  		SyncStderr: os.Stderr,
    76  	})
    77  
    78  	// Request the client
    79  	client, err := pluginClient.Client()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	// Get the app implementation
    85  	appImpl, err := client.App()
    86  	if err != nil {
    87  		return err
    88  	}
    89  	if c, ok := appImpl.(io.Closer); ok {
    90  		defer c.Close()
    91  	}
    92  
    93  	p.AppMeta, err = appImpl.Meta()
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	// Create a custom factory that when called marks the plugin as used
    99  	p.used = false
   100  	p.App = func() (app.App, error) {
   101  		p.used = true
   102  		return client.App()
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // Used tracks whether or not this plugin was used or not. You can call
   109  // this after compilation on each plugin to determine what plugin
   110  // was used.
   111  func (p *Plugin) Used() bool {
   112  	return p.used
   113  }
   114  
   115  func (p *Plugin) String() string {
   116  	path := p.Path
   117  	if p.Builtin {
   118  		path = "<builtin>"
   119  	}
   120  
   121  	return fmt.Sprintf("%s %v", path, p.Args)
   122  }
   123  
   124  // ConfigureCore configures the Otto core configuration with the loaded
   125  // plugin data.
   126  func (m *PluginManager) ConfigureCore(core *otto.CoreConfig) error {
   127  	if core.Apps == nil {
   128  		core.Apps = make(map[app.Tuple]app.Factory)
   129  	}
   130  
   131  	for _, p := range m.Plugins() {
   132  		for _, tuple := range p.AppMeta.Tuples {
   133  			core.Apps[tuple] = p.App
   134  		}
   135  	}
   136  
   137  	return nil
   138  }
   139  
   140  // Plugins returns the loaded plugins.
   141  func (m *PluginManager) Plugins() []*Plugin {
   142  	return m.plugins
   143  }
   144  
   145  // Discover will find all the available plugin binaries. Each time this
   146  // is called it will override any previously discovered plugins.
   147  func (m *PluginManager) Discover() error {
   148  	result := make([]*Plugin, 0, 20)
   149  
   150  	if !testingMode {
   151  		// First we add all the builtin plugins which we get by executing ourself
   152  		for k, _ := range m.PluginMap {
   153  			result = append(result, &Plugin{
   154  				Args:    []string{"plugin-builtin", k},
   155  				Builtin: true,
   156  			})
   157  		}
   158  	}
   159  
   160  	for _, dir := range m.PluginDirs {
   161  		log.Printf("[DEBUG] Looking for plugins in: %s", dir)
   162  		paths, err := plugin.Discover(PluginGlob, dir)
   163  		if err != nil {
   164  			return fmt.Errorf(
   165  				"Error discovering plugins in %s: %s", dir, err)
   166  		}
   167  
   168  		for _, path := range paths {
   169  			result = append(result, &Plugin{
   170  				Path: path,
   171  			})
   172  		}
   173  	}
   174  
   175  	// Reverse the list of plugins. We do this because we want custom
   176  	// plugins to take priority over built-in plugins, and the PluginDirs
   177  	// ordering also defines this priority.
   178  	for left, right := 0, len(result)-1; left < right; left, right = left+1, right-1 {
   179  		result[left], result[right] = result[right], result[left]
   180  	}
   181  
   182  	// Log it
   183  	for _, r := range result {
   184  		log.Printf("[DEBUG] Detected plugin: %s", r)
   185  	}
   186  
   187  	// Save our result
   188  	m.plugins = result
   189  
   190  	return nil
   191  }
   192  
   193  // StoreUsed will persist the used plugins into a file. LoadUsed can
   194  // then be called to load the plugins that were used only, making plugin
   195  // loading much more efficient.
   196  func (m *PluginManager) StoreUsed(path string) error {
   197  	// Get the used plugins
   198  	plugins := make([]*Plugin, 0, 2)
   199  	for _, p := range m.Plugins() {
   200  		if p.Used() {
   201  			plugins = append(plugins, p)
   202  		}
   203  	}
   204  
   205  	// Write the used plugins to the given path as JSON
   206  	f, err := os.Create(path)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer f.Close()
   211  
   212  	enc := json.NewEncoder(f)
   213  	return enc.Encode(&usedPluginWrapper{
   214  		Version: usedPluginVersion,
   215  		Plugins: plugins,
   216  	})
   217  }
   218  
   219  // LoadUsed will load the plugins in the given used file that was saved
   220  // with StoreUsed.
   221  func (m *PluginManager) LoadUsed(path string) error {
   222  	f, err := os.Open(path)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	var wrapper usedPluginWrapper
   228  	dec := json.NewDecoder(f)
   229  	err = dec.Decode(&wrapper)
   230  	f.Close()
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	if wrapper.Version > usedPluginVersion {
   236  		return fmt.Errorf(
   237  			"Couldn't load used plugins because the format of the stored\n" +
   238  				"metadata is newer than this version of Otto knows how to read.\n\n" +
   239  				"This is usually caused by a newer version of Otto compiling an\n" +
   240  				"environment. Please use a later version of Otto to read this.")
   241  	}
   242  
   243  	m.plugins = wrapper.Plugins
   244  	return m.LoadAll()
   245  }
   246  
   247  // LoadAll will launch every plugin and add it to the CoreConfig given.
   248  func (m *PluginManager) LoadAll() error {
   249  	// If we've never loaded plugin paths, then let's discover those first
   250  	if m.Plugins() == nil {
   251  		if err := m.Discover(); err != nil {
   252  			return err
   253  		}
   254  	}
   255  
   256  	// Go through each plugin path and load single
   257  	var merr error
   258  	var merrLock sync.Mutex
   259  	var wg sync.WaitGroup
   260  	sema := semaphore.New(runtime.NumCPU())
   261  	for _, plugin := range m.Plugins() {
   262  		wg.Add(1)
   263  		go func(plugin *Plugin) {
   264  			defer wg.Done()
   265  
   266  			sema.Acquire()
   267  			defer sema.Release()
   268  
   269  			if err := plugin.Load(); err != nil {
   270  				merrLock.Lock()
   271  				defer merrLock.Unlock()
   272  				merr = multierror.Append(merr, fmt.Errorf(
   273  					"Error loading plugin %s: %s",
   274  					plugin.Path, err))
   275  			}
   276  		}(plugin)
   277  	}
   278  
   279  	// Wait for all the plugins to load
   280  	wg.Wait()
   281  
   282  	return merr
   283  }
   284  
   285  // usedPluginVersion is the current version of the used plugin format
   286  // that we understand. We can increment and handle older versions as we go.
   287  const usedPluginVersion int = 1
   288  
   289  type usedPluginWrapper struct {
   290  	Version int       `json:"version"`
   291  	Plugins []*Plugin `json:"plugins"`
   292  }
   293  
   294  // pluginExePath is our own path. We cache this so we only have to calculate
   295  // it once.
   296  var pluginExePath string
   297  
   298  func init() {
   299  	var err error
   300  	pluginExePath, err = osext.Executable()
   301  	if err != nil {
   302  		panic(err)
   303  	}
   304  }