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  }