github.com/turgay/mattermost-server@v5.3.2-0.20181002173352-2945e8a2b0ce+incompatible/plugin/environment.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package plugin 5 6 import ( 7 "fmt" 8 "hash/fnv" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "sync" 13 14 "github.com/mattermost/mattermost-server/mlog" 15 "github.com/mattermost/mattermost-server/model" 16 "github.com/mattermost/mattermost-server/utils" 17 "github.com/pkg/errors" 18 ) 19 20 type apiImplCreatorFunc func(*model.Manifest) API 21 22 // multiPluginHookRunnerFunc is a callback function to invoke as part of RunMultiPluginHook. 23 // 24 // Return false to stop the hook from iterating to subsequent plugins. 25 type multiPluginHookRunnerFunc func(hooks Hooks) bool 26 27 type activePlugin struct { 28 BundleInfo *model.BundleInfo 29 State int 30 31 supervisor *supervisor 32 } 33 34 // Environment represents the execution environment of active plugins. 35 // 36 // It is meant for use by the Mattermost server to manipulate, interact with and report on the set 37 // of active plugins. 38 type Environment struct { 39 activePlugins sync.Map 40 logger *mlog.Logger 41 newAPIImpl apiImplCreatorFunc 42 pluginDir string 43 webappPluginDir string 44 } 45 46 func NewEnvironment(newAPIImpl apiImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) { 47 return &Environment{ 48 logger: logger, 49 newAPIImpl: newAPIImpl, 50 pluginDir: pluginDir, 51 webappPluginDir: webappPluginDir, 52 }, nil 53 } 54 55 // Performs a full scan of the given path. 56 // 57 // This function will return info for all subdirectories that appear to be plugins (i.e. all 58 // subdirectories containing plugin manifest files, regardless of whether they could actually be 59 // parsed). 60 // 61 // Plugins are found non-recursively and paths beginning with a dot are always ignored. 62 func scanSearchPath(path string) ([]*model.BundleInfo, error) { 63 files, err := ioutil.ReadDir(path) 64 if err != nil { 65 return nil, err 66 } 67 var ret []*model.BundleInfo 68 for _, file := range files { 69 if !file.IsDir() || file.Name()[0] == '.' { 70 continue 71 } 72 if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { 73 ret = append(ret, info) 74 } 75 } 76 return ret, nil 77 } 78 79 // Returns a list of all plugins within the environment. 80 func (env *Environment) Available() ([]*model.BundleInfo, error) { 81 return scanSearchPath(env.pluginDir) 82 } 83 84 // Returns a list of all currently active plugins within the environment. 85 func (env *Environment) Active() []*model.BundleInfo { 86 activePlugins := []*model.BundleInfo{} 87 env.activePlugins.Range(func(key, value interface{}) bool { 88 activePlugins = append(activePlugins, value.(activePlugin).BundleInfo) 89 90 return true 91 }) 92 93 return activePlugins 94 } 95 96 // IsActive returns true if the plugin with the given id is active. 97 func (env *Environment) IsActive(id string) bool { 98 _, ok := env.activePlugins.Load(id) 99 return ok 100 } 101 102 // Statuses returns a list of plugin statuses representing the state of every plugin 103 func (env *Environment) Statuses() (model.PluginStatuses, error) { 104 plugins, err := env.Available() 105 if err != nil { 106 return nil, errors.Wrap(err, "unable to get plugin statuses") 107 } 108 109 pluginStatuses := make(model.PluginStatuses, 0, len(plugins)) 110 for _, plugin := range plugins { 111 // For now we don't handle bad manifests, we should 112 if plugin.Manifest == nil { 113 continue 114 } 115 116 pluginState := model.PluginStateNotRunning 117 if plugin, ok := env.activePlugins.Load(plugin.Manifest.Id); ok { 118 pluginState = plugin.(activePlugin).State 119 } 120 121 status := &model.PluginStatus{ 122 PluginId: plugin.Manifest.Id, 123 PluginPath: filepath.Dir(plugin.ManifestPath), 124 State: pluginState, 125 Name: plugin.Manifest.Name, 126 Description: plugin.Manifest.Description, 127 Version: plugin.Manifest.Version, 128 } 129 130 pluginStatuses = append(pluginStatuses, status) 131 } 132 133 return pluginStatuses, nil 134 } 135 136 func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) { 137 // Check if we are already active 138 if _, ok := env.activePlugins.Load(id); ok { 139 return nil, false, nil 140 } 141 142 plugins, err := env.Available() 143 if err != nil { 144 return nil, false, err 145 } 146 var pluginInfo *model.BundleInfo 147 for _, p := range plugins { 148 if p.Manifest != nil && p.Manifest.Id == id { 149 if pluginInfo != nil { 150 return nil, false, fmt.Errorf("multiple plugins found: %v", id) 151 } 152 pluginInfo = p 153 } 154 } 155 if pluginInfo == nil { 156 return nil, false, fmt.Errorf("plugin not found: %v", id) 157 } 158 159 activePlugin := activePlugin{BundleInfo: pluginInfo} 160 defer func() { 161 if reterr == nil { 162 activePlugin.State = model.PluginStateRunning 163 } else { 164 activePlugin.State = model.PluginStateFailedToStart 165 } 166 env.activePlugins.Store(pluginInfo.Manifest.Id, activePlugin) 167 }() 168 169 componentActivated := false 170 171 if pluginInfo.Manifest.HasWebapp() { 172 bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath) 173 if bundlePath == "" || bundlePath[0] == '.' { 174 return nil, false, fmt.Errorf("invalid webapp bundle path") 175 } 176 bundlePath = filepath.Join(env.pluginDir, id, bundlePath) 177 destinationPath := filepath.Join(env.webappPluginDir, id) 178 179 if err := os.RemoveAll(destinationPath); err != nil { 180 return nil, false, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath) 181 } 182 183 if err := utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil { 184 return nil, false, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id) 185 } 186 187 sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath)) 188 189 sourceBundleFileContents, err := ioutil.ReadFile(sourceBundleFilepath) 190 if err != nil { 191 return nil, false, errors.Wrapf(err, "unable to read webapp bundle: %v", id) 192 } 193 194 hash := fnv.New64a() 195 hash.Write(sourceBundleFileContents) 196 pluginInfo.Manifest.Webapp.BundleHash = hash.Sum([]byte{}) 197 198 if err := os.Rename( 199 sourceBundleFilepath, 200 filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, pluginInfo.Manifest.Webapp.BundleHash)), 201 ); err != nil { 202 return nil, false, errors.Wrapf(err, "unable to rename webapp bundle: %v", id) 203 } 204 205 componentActivated = true 206 } 207 208 if pluginInfo.Manifest.HasServer() { 209 supervisor, err := newSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest)) 210 if err != nil { 211 return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id) 212 } 213 activePlugin.supervisor = supervisor 214 215 componentActivated = true 216 } 217 218 if !componentActivated { 219 return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component") 220 } 221 222 return pluginInfo.Manifest, true, nil 223 } 224 225 // Deactivates the plugin with the given id. 226 func (env *Environment) Deactivate(id string) bool { 227 p, ok := env.activePlugins.Load(id) 228 if !ok { 229 return false 230 } 231 232 env.activePlugins.Delete(id) 233 234 activePlugin := p.(activePlugin) 235 if activePlugin.supervisor != nil { 236 if err := activePlugin.supervisor.Hooks().OnDeactivate(); err != nil { 237 env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err)) 238 } 239 activePlugin.supervisor.Shutdown() 240 } 241 242 return true 243 } 244 245 // Shutdown deactivates all plugins and gracefully shuts down the environment. 246 func (env *Environment) Shutdown() { 247 env.activePlugins.Range(func(key, value interface{}) bool { 248 activePlugin := value.(activePlugin) 249 250 if activePlugin.supervisor != nil { 251 if err := activePlugin.supervisor.Hooks().OnDeactivate(); err != nil { 252 env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err)) 253 } 254 activePlugin.supervisor.Shutdown() 255 } 256 257 env.activePlugins.Delete(key) 258 259 return true 260 }) 261 } 262 263 // HooksForPlugin returns the hooks API for the plugin with the given id. 264 // 265 // Consider using RunMultiPluginHook instead. 266 func (env *Environment) HooksForPlugin(id string) (Hooks, error) { 267 if p, ok := env.activePlugins.Load(id); ok { 268 activePlugin := p.(activePlugin) 269 if activePlugin.supervisor != nil { 270 return activePlugin.supervisor.Hooks(), nil 271 } 272 } 273 274 return nil, fmt.Errorf("plugin not found: %v", id) 275 } 276 277 // RunMultiPluginHook invokes hookRunnerFunc for each plugin that implements the given hookId. 278 // 279 // If hookRunnerFunc returns false, iteration will not continue. The iteration order among active 280 // plugins is not specified. 281 func (env *Environment) RunMultiPluginHook(hookRunnerFunc multiPluginHookRunnerFunc, hookId int) { 282 env.activePlugins.Range(func(key, value interface{}) bool { 283 activePlugin := value.(activePlugin) 284 285 if activePlugin.supervisor == nil || !activePlugin.supervisor.Implements(hookId) { 286 return true 287 } 288 if !hookRunnerFunc(activePlugin.supervisor.Hooks()) { 289 return false 290 } 291 292 return true 293 }) 294 }