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  }