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