launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/cmd/juju/plugin.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  
    16  	"launchpad.net/gnuflag"
    17  	"launchpad.net/juju-core/cmd"
    18  	"launchpad.net/juju-core/juju/osenv"
    19  	"launchpad.net/juju-core/log"
    20  )
    21  
    22  const JujuPluginPrefix = "juju-"
    23  
    24  // This is a very rudimentary method used to extract common Juju
    25  // arguments from the full list passed to the plugin. Currently,
    26  // there is only one such argument: -e env
    27  // If more than just -e is required, the method can be improved then.
    28  func extractJujuArgs(args []string) []string {
    29  	var jujuArgs []string
    30  	nrArgs := len(args)
    31  	for nextArg := 0; nextArg < nrArgs; {
    32  		arg := args[nextArg]
    33  		nextArg++
    34  		if arg != "-e" {
    35  			continue
    36  		}
    37  		jujuArgs = append(jujuArgs, arg)
    38  		if nextArg < nrArgs {
    39  			jujuArgs = append(jujuArgs, args[nextArg])
    40  			nextArg++
    41  		}
    42  	}
    43  	return jujuArgs
    44  }
    45  
    46  func RunPlugin(ctx *cmd.Context, subcommand string, args []string) error {
    47  	cmdName := JujuPluginPrefix + subcommand
    48  	plugin := &PluginCommand{name: cmdName}
    49  
    50  	// We process common flags supported by Juju commands.
    51  	// To do this, we extract only those supported flags from the
    52  	// argument list to avoid confusing flags.Parse().
    53  	flags := gnuflag.NewFlagSet(cmdName, gnuflag.ContinueOnError)
    54  	flags.SetOutput(ioutil.Discard)
    55  	plugin.SetFlags(flags)
    56  	jujuArgs := extractJujuArgs(args)
    57  	err := flags.Parse(false, jujuArgs)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	plugin.Init(args)
    63  	err = plugin.Run(ctx)
    64  	_, execError := err.(*exec.Error)
    65  	// exec.Error results are for when the executable isn't found, in
    66  	// those cases, drop through.
    67  	if !execError {
    68  		return err
    69  	}
    70  	return &cmd.UnrecognizedCommand{subcommand}
    71  }
    72  
    73  type PluginCommand struct {
    74  	cmd.EnvCommandBase
    75  	name string
    76  	args []string
    77  }
    78  
    79  // Info is just a stub so that PluginCommand implements cmd.Command.
    80  // Since this is never actually called, we can happily return nil.
    81  func (*PluginCommand) Info() *cmd.Info {
    82  	return nil
    83  }
    84  
    85  func (c *PluginCommand) Init(args []string) error {
    86  	c.args = args
    87  	return nil
    88  }
    89  
    90  func (c *PluginCommand) SetFlags(f *gnuflag.FlagSet) {
    91  	c.EnvCommandBase.SetFlags(f)
    92  }
    93  
    94  func (c *PluginCommand) Run(ctx *cmd.Context) error {
    95  	command := exec.Command(c.name, c.args...)
    96  	command.Env = append(os.Environ(), []string{
    97  		osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(),
    98  		osenv.JujuEnvEnvKey + "=" + c.EnvironName()}...,
    99  	)
   100  
   101  	// Now hook up stdin, stdout, stderr
   102  	command.Stdin = ctx.Stdin
   103  	command.Stdout = ctx.Stdout
   104  	command.Stderr = ctx.Stderr
   105  	// And run it!
   106  	return command.Run()
   107  }
   108  
   109  type PluginDescription struct {
   110  	name        string
   111  	description string
   112  }
   113  
   114  const PluginTopicText = `Juju Plugins
   115  
   116  Plugins are implemented as stand-alone executable files somewhere in the user's PATH.
   117  The executable command must be of the format juju-<plugin name>.
   118  
   119  `
   120  
   121  func PluginHelpTopic() string {
   122  	output := &bytes.Buffer{}
   123  	fmt.Fprintf(output, PluginTopicText)
   124  
   125  	existingPlugins := GetPluginDescriptions()
   126  
   127  	if len(existingPlugins) == 0 {
   128  		fmt.Fprintf(output, "No plugins found.\n")
   129  	} else {
   130  		longest := 0
   131  		for _, plugin := range existingPlugins {
   132  			if len(plugin.name) > longest {
   133  				longest = len(plugin.name)
   134  			}
   135  		}
   136  		for _, plugin := range existingPlugins {
   137  			fmt.Fprintf(output, "%-*s  %s\n", longest, plugin.name, plugin.description)
   138  		}
   139  	}
   140  
   141  	return output.String()
   142  }
   143  
   144  // GetPluginDescriptions runs each plugin with "--description".  The calls to
   145  // the plugins are run in parallel, so the function should only take as long
   146  // as the longest call.
   147  func GetPluginDescriptions() []PluginDescription {
   148  	plugins := findPlugins()
   149  	results := []PluginDescription{}
   150  	if len(plugins) == 0 {
   151  		return results
   152  	}
   153  	// create a channel with enough backing for each plugin
   154  	description := make(chan PluginDescription, len(plugins))
   155  
   156  	// exec the command, and wait only for the timeout before killing the process
   157  	for _, plugin := range plugins {
   158  		go func(plugin string) {
   159  			result := PluginDescription{name: plugin}
   160  			defer func() {
   161  				description <- result
   162  			}()
   163  			desccmd := exec.Command(plugin, "--description")
   164  			output, err := desccmd.CombinedOutput()
   165  
   166  			if err == nil {
   167  				// trim to only get the first line
   168  				result.description = strings.SplitN(string(output), "\n", 2)[0]
   169  			} else {
   170  				result.description = fmt.Sprintf("error occurred running '%s --description'", plugin)
   171  				log.Errorf("'%s --description': %s", plugin, err)
   172  			}
   173  		}(plugin)
   174  	}
   175  	resultMap := map[string]PluginDescription{}
   176  	// gather the results at the end
   177  	for _ = range plugins {
   178  		result := <-description
   179  		resultMap[result.name] = result
   180  	}
   181  	// plugins array is already sorted, use this to get the results in order
   182  	for _, plugin := range plugins {
   183  		// Strip the 'juju-' off the start of the plugin name in the results
   184  		result := resultMap[plugin]
   185  		result.name = result.name[len(JujuPluginPrefix):]
   186  		results = append(results, result)
   187  	}
   188  	return results
   189  }
   190  
   191  // findPlugins searches the current PATH for executable files that start with
   192  // JujuPluginPrefix.
   193  func findPlugins() []string {
   194  	path := os.Getenv("PATH")
   195  	plugins := []string{}
   196  	for _, name := range filepath.SplitList(path) {
   197  		entries, err := ioutil.ReadDir(name)
   198  		if err != nil {
   199  			continue
   200  		}
   201  		for _, entry := range entries {
   202  			if strings.HasPrefix(entry.Name(), JujuPluginPrefix) && (entry.Mode()&0111) != 0 {
   203  				plugins = append(plugins, entry.Name())
   204  			}
   205  		}
   206  	}
   207  	sort.Strings(plugins)
   208  	return plugins
   209  }