github.com/bhameyie/otto@v0.2.1-0.20160406174117-16052efa52ec/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 }