github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/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 "time" 14 15 "github.com/pkg/errors" 16 17 "github.com/mattermost/mattermost-server/v5/einterfaces" 18 "github.com/mattermost/mattermost-server/v5/mlog" 19 "github.com/mattermost/mattermost-server/v5/model" 20 "github.com/mattermost/mattermost-server/v5/utils" 21 ) 22 23 var ErrNotFound = errors.New("Item not found") 24 25 type apiImplCreatorFunc func(*model.Manifest) API 26 27 // registeredPlugin stores the state for a given plugin that has been activated 28 // or attempted to be activated this server run. 29 // 30 // If an installed plugin is missing from the env.registeredPlugins map, then the 31 // plugin is configured as disabled and has not been activated during this server run. 32 type registeredPlugin struct { 33 BundleInfo *model.BundleInfo 34 State int 35 36 supervisor *supervisor 37 } 38 39 // PrepackagedPlugin is a plugin prepackaged with the server and found on startup. 40 type PrepackagedPlugin struct { 41 Path string 42 IconData string 43 Manifest *model.Manifest 44 Signature []byte 45 } 46 47 // Environment represents the execution environment of active plugins. 48 // 49 // It is meant for use by the Mattermost server to manipulate, interact with and report on the set 50 // of active plugins. 51 type Environment struct { 52 registeredPlugins sync.Map 53 pluginHealthCheckJob *PluginHealthCheckJob 54 logger *mlog.Logger 55 metrics einterfaces.MetricsInterface 56 newAPIImpl apiImplCreatorFunc 57 pluginDir string 58 webappPluginDir string 59 prepackagedPlugins []*PrepackagedPlugin 60 prepackagedPluginsLock sync.RWMutex 61 } 62 63 func NewEnvironment(newAPIImpl apiImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger, metrics einterfaces.MetricsInterface) (*Environment, error) { 64 return &Environment{ 65 logger: logger, 66 metrics: metrics, 67 newAPIImpl: newAPIImpl, 68 pluginDir: pluginDir, 69 webappPluginDir: webappPluginDir, 70 }, nil 71 } 72 73 // Performs a full scan of the given path. 74 // 75 // This function will return info for all subdirectories that appear to be plugins (i.e. all 76 // subdirectories containing plugin manifest files, regardless of whether they could actually be 77 // parsed). 78 // 79 // Plugins are found non-recursively and paths beginning with a dot are always ignored. 80 func scanSearchPath(path string) ([]*model.BundleInfo, error) { 81 files, err := ioutil.ReadDir(path) 82 if err != nil { 83 return nil, err 84 } 85 var ret []*model.BundleInfo 86 for _, file := range files { 87 if !file.IsDir() || file.Name()[0] == '.' { 88 continue 89 } 90 info := model.BundleInfoForPath(filepath.Join(path, file.Name())) 91 if info.Manifest != nil { 92 ret = append(ret, info) 93 } 94 } 95 return ret, nil 96 } 97 98 // Returns a list of all plugins within the environment. 99 func (env *Environment) Available() ([]*model.BundleInfo, error) { 100 return scanSearchPath(env.pluginDir) 101 } 102 103 // Returns a list of prepackaged plugins available in the local prepackaged_plugins folder. 104 // The list content is immutable and should not be modified. 105 func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin { 106 env.prepackagedPluginsLock.RLock() 107 defer env.prepackagedPluginsLock.RUnlock() 108 109 return env.prepackagedPlugins 110 } 111 112 // Returns a list of all currently active plugins within the environment. 113 // The returned list should not be modified. 114 func (env *Environment) Active() []*model.BundleInfo { 115 activePlugins := []*model.BundleInfo{} 116 env.registeredPlugins.Range(func(key, value interface{}) bool { 117 plugin := value.(registeredPlugin) 118 if env.IsActive(plugin.BundleInfo.Manifest.Id) { 119 activePlugins = append(activePlugins, plugin.BundleInfo) 120 } 121 122 return true 123 }) 124 125 return activePlugins 126 } 127 128 // IsActive returns true if the plugin with the given id is active. 129 func (env *Environment) IsActive(id string) bool { 130 return env.GetPluginState(id) == model.PluginStateRunning 131 } 132 133 // GetPluginState returns the current state of a plugin (disabled, running, or error) 134 func (env *Environment) GetPluginState(id string) int { 135 rp, ok := env.registeredPlugins.Load(id) 136 if !ok { 137 return model.PluginStateNotRunning 138 } 139 140 return rp.(registeredPlugin).State 141 } 142 143 // setPluginState sets the current state of a plugin (disabled, running, or error) 144 func (env *Environment) setPluginState(id string, state int) { 145 if rp, ok := env.registeredPlugins.Load(id); ok { 146 p := rp.(registeredPlugin) 147 p.State = state 148 env.registeredPlugins.Store(id, p) 149 } 150 } 151 152 // PublicFilesPath returns a path and true if the plugin with the given id is active. 153 // It returns an empty string and false if the path is not set or invalid 154 func (env *Environment) PublicFilesPath(id string) (string, error) { 155 if !env.IsActive(id) { 156 return "", fmt.Errorf("plugin not found: %v", id) 157 } 158 return filepath.Join(env.pluginDir, id, "public"), nil 159 } 160 161 // Statuses returns a list of plugin statuses representing the state of every plugin 162 func (env *Environment) Statuses() (model.PluginStatuses, error) { 163 plugins, err := env.Available() 164 if err != nil { 165 return nil, errors.Wrap(err, "unable to get plugin statuses") 166 } 167 168 pluginStatuses := make(model.PluginStatuses, 0, len(plugins)) 169 for _, plugin := range plugins { 170 // For now we don't handle bad manifests, we should 171 if plugin.Manifest == nil { 172 continue 173 } 174 175 pluginState := env.GetPluginState(plugin.Manifest.Id) 176 177 status := &model.PluginStatus{ 178 PluginId: plugin.Manifest.Id, 179 PluginPath: filepath.Dir(plugin.ManifestPath), 180 State: pluginState, 181 Name: plugin.Manifest.Name, 182 Description: plugin.Manifest.Description, 183 Version: plugin.Manifest.Version, 184 } 185 186 pluginStatuses = append(pluginStatuses, status) 187 } 188 189 return pluginStatuses, nil 190 } 191 192 // GetManifest returns a manifest for a given pluginId. 193 // Returns ErrNotFound if plugin is not found. 194 func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) { 195 plugins, err := env.Available() 196 if err != nil { 197 return nil, errors.Wrap(err, "unable to get plugin statuses") 198 } 199 200 for _, plugin := range plugins { 201 if plugin.Manifest != nil && plugin.Manifest.Id == pluginId { 202 return plugin.Manifest, nil 203 } 204 } 205 206 return nil, ErrNotFound 207 } 208 209 func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) { 210 // Check if we are already active 211 if env.IsActive(id) { 212 return nil, false, nil 213 } 214 215 plugins, err := env.Available() 216 if err != nil { 217 return nil, false, err 218 } 219 var pluginInfo *model.BundleInfo 220 for _, p := range plugins { 221 if p.Manifest != nil && p.Manifest.Id == id { 222 if pluginInfo != nil { 223 return nil, false, fmt.Errorf("multiple plugins found: %v", id) 224 } 225 pluginInfo = p 226 } 227 } 228 if pluginInfo == nil { 229 return nil, false, fmt.Errorf("plugin not found: %v", id) 230 } 231 232 rp := newRegisteredPlugin(pluginInfo) 233 env.registeredPlugins.Store(id, rp) 234 235 defer func() { 236 if reterr == nil { 237 env.setPluginState(id, model.PluginStateRunning) 238 } else { 239 env.setPluginState(id, model.PluginStateFailedToStart) 240 } 241 }() 242 243 if pluginInfo.Manifest.MinServerVersion != "" { 244 fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion) 245 if err != nil { 246 return nil, false, fmt.Errorf("%v: %v", err.Error(), id) 247 } 248 if !fulfilled { 249 return nil, false, fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, id) 250 } 251 } 252 253 componentActivated := false 254 255 if pluginInfo.Manifest.HasWebapp() { 256 updatedManifest, err := env.UnpackWebappBundle(id) 257 if err != nil { 258 return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id) 259 } 260 pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash 261 262 componentActivated = true 263 } 264 265 if pluginInfo.Manifest.HasServer() { 266 sup, err := newSupervisor(pluginInfo, env.newAPIImpl(pluginInfo.Manifest), env.logger, env.metrics) 267 if err != nil { 268 return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id) 269 } 270 271 if err := sup.Hooks().OnActivate(); err != nil { 272 sup.Shutdown() 273 return nil, false, err 274 } 275 rp.supervisor = sup 276 env.registeredPlugins.Store(id, rp) 277 278 componentActivated = true 279 } 280 281 if !componentActivated { 282 return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component") 283 } 284 285 return pluginInfo.Manifest, true, nil 286 } 287 288 func (env *Environment) RemovePlugin(id string) { 289 if _, ok := env.registeredPlugins.Load(id); ok { 290 env.registeredPlugins.Delete(id) 291 } 292 } 293 294 // Deactivates the plugin with the given id. 295 func (env *Environment) Deactivate(id string) bool { 296 p, ok := env.registeredPlugins.Load(id) 297 if !ok { 298 return false 299 } 300 301 isActive := env.IsActive(id) 302 303 env.setPluginState(id, model.PluginStateNotRunning) 304 305 if !isActive { 306 return false 307 } 308 309 rp := p.(registeredPlugin) 310 if rp.supervisor != nil { 311 if err := rp.supervisor.Hooks().OnDeactivate(); err != nil { 312 env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err)) 313 } 314 rp.supervisor.Shutdown() 315 } 316 317 return true 318 } 319 320 // RestartPlugin deactivates, then activates the plugin with the given id. 321 func (env *Environment) RestartPlugin(id string) error { 322 env.Deactivate(id) 323 _, _, err := env.Activate(id) 324 return err 325 } 326 327 // Shutdown deactivates all plugins and gracefully shuts down the environment. 328 func (env *Environment) Shutdown() { 329 if env.pluginHealthCheckJob != nil { 330 env.pluginHealthCheckJob.Cancel() 331 } 332 333 var wg sync.WaitGroup 334 env.registeredPlugins.Range(func(key, value interface{}) bool { 335 rp := value.(registeredPlugin) 336 337 if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) { 338 return true 339 } 340 341 wg.Add(1) 342 343 done := make(chan bool) 344 go func() { 345 defer close(done) 346 if err := rp.supervisor.Hooks().OnDeactivate(); err != nil { 347 env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err)) 348 } 349 }() 350 351 go func() { 352 defer wg.Done() 353 354 select { 355 case <-time.After(10 * time.Second): 356 env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id)) 357 case <-done: 358 } 359 360 rp.supervisor.Shutdown() 361 }() 362 363 return true 364 }) 365 366 wg.Wait() 367 368 env.registeredPlugins.Range(func(key, value interface{}) bool { 369 env.registeredPlugins.Delete(key) 370 371 return true 372 }) 373 } 374 375 // UnpackWebappBundle unpacks webapp bundle for a given plugin id on disk. 376 func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) { 377 plugins, err := env.Available() 378 if err != nil { 379 return nil, errors.New("Unable to get available plugins") 380 } 381 var manifest *model.Manifest 382 for _, p := range plugins { 383 if p.Manifest != nil && p.Manifest.Id == id { 384 if manifest != nil { 385 return nil, fmt.Errorf("multiple plugins found: %v", id) 386 } 387 manifest = p.Manifest 388 } 389 } 390 if manifest == nil { 391 return nil, fmt.Errorf("plugin not found: %v", id) 392 } 393 394 bundlePath := filepath.Clean(manifest.Webapp.BundlePath) 395 if bundlePath == "" || bundlePath[0] == '.' { 396 return nil, fmt.Errorf("invalid webapp bundle path") 397 } 398 bundlePath = filepath.Join(env.pluginDir, id, bundlePath) 399 destinationPath := filepath.Join(env.webappPluginDir, id) 400 401 if err = os.RemoveAll(destinationPath); err != nil { 402 return nil, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath) 403 } 404 405 if err = utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil { 406 return nil, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id) 407 } 408 409 sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath)) 410 411 sourceBundleFileContents, err := ioutil.ReadFile(sourceBundleFilepath) 412 if err != nil { 413 return nil, errors.Wrapf(err, "unable to read webapp bundle: %v", id) 414 } 415 416 hash := fnv.New64a() 417 if _, err = hash.Write(sourceBundleFileContents); err != nil { 418 return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id) 419 } 420 manifest.Webapp.BundleHash = hash.Sum([]byte{}) 421 422 if err = os.Rename( 423 sourceBundleFilepath, 424 filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, manifest.Webapp.BundleHash)), 425 ); err != nil { 426 return nil, errors.Wrapf(err, "unable to rename webapp bundle: %v", id) 427 } 428 429 return manifest, nil 430 } 431 432 // HooksForPlugin returns the hooks API for the plugin with the given id. 433 // 434 // Consider using RunMultiPluginHook instead. 435 func (env *Environment) HooksForPlugin(id string) (Hooks, error) { 436 if p, ok := env.registeredPlugins.Load(id); ok { 437 rp := p.(registeredPlugin) 438 if rp.supervisor != nil && env.IsActive(id) { 439 return rp.supervisor.Hooks(), nil 440 } 441 } 442 443 return nil, fmt.Errorf("plugin not found: %v", id) 444 } 445 446 // RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId. 447 // 448 // If hookRunnerFunc returns false, iteration will not continue. The iteration order among active 449 // plugins is not specified. 450 func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool, hookId int) { 451 startTime := time.Now() 452 453 env.registeredPlugins.Range(func(key, value interface{}) bool { 454 rp := value.(registeredPlugin) 455 456 if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) { 457 return true 458 } 459 460 hookStartTime := time.Now() 461 result := hookRunnerFunc(rp.supervisor.Hooks()) 462 463 if env.metrics != nil { 464 elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second) 465 env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime) 466 } 467 468 return result 469 }) 470 471 if env.metrics != nil { 472 elapsedTime := float64(time.Since(startTime)) / float64(time.Second) 473 env.metrics.ObservePluginMultiHookDuration(elapsedTime) 474 } 475 } 476 477 // PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed. 478 func (env *Environment) PerformHealthCheck(id string) error { 479 p, ok := env.registeredPlugins.Load(id) 480 if !ok { 481 return nil 482 } 483 rp := p.(registeredPlugin) 484 485 sup := rp.supervisor 486 if sup == nil { 487 return nil 488 } 489 return sup.PerformHealthCheck() 490 } 491 492 // SetPrepackagedPlugins saves prepackaged plugins in the environment. 493 func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) { 494 env.prepackagedPluginsLock.Lock() 495 env.prepackagedPlugins = plugins 496 env.prepackagedPluginsLock.Unlock() 497 } 498 499 func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin { 500 state := model.PluginStateNotRunning 501 return registeredPlugin{State: state, BundleInfo: bundle} 502 } 503 504 // InitPluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled. 505 func (env *Environment) InitPluginHealthCheckJob(enable bool) { 506 // Config is set to enable. No job exists, start a new job. 507 if enable && env.pluginHealthCheckJob == nil { 508 mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval)) 509 510 job := newPluginHealthCheckJob(env) 511 env.pluginHealthCheckJob = job 512 go job.run() 513 } 514 515 // Config is set to disable. Job exists, kill existing job. 516 if !enable && env.pluginHealthCheckJob != nil { 517 mlog.Debug("Disabling plugin health check job") 518 519 env.pluginHealthCheckJob.Cancel() 520 env.pluginHealthCheckJob = nil 521 } 522 } 523 524 // GetPluginHealthCheckJob returns the configured PluginHealthCheckJob, if any. 525 func (env *Environment) GetPluginHealthCheckJob() *PluginHealthCheckJob { 526 return env.pluginHealthCheckJob 527 }