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