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 }