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