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  }