github.com/coincircle/mattermost-server@v4.8.1-0.20180321182714-9d701c704416+incompatible/plugin/pluginenv/environment.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  // Package pluginenv provides high level functionality for discovering and launching plugins.
     5  package pluginenv
     6  
     7  import (
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"path/filepath"
    12  	"sync"
    13  
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/mattermost/mattermost-server/model"
    17  	"github.com/mattermost/mattermost-server/plugin"
    18  )
    19  
    20  type APIProviderFunc func(*model.Manifest) (plugin.API, error)
    21  type SupervisorProviderFunc func(*model.BundleInfo) (plugin.Supervisor, error)
    22  
    23  type ActivePlugin struct {
    24  	BundleInfo *model.BundleInfo
    25  	Supervisor plugin.Supervisor
    26  }
    27  
    28  // Environment represents an environment that plugins are discovered and launched in.
    29  type Environment struct {
    30  	searchPath         string
    31  	webappPath         string
    32  	apiProvider        APIProviderFunc
    33  	supervisorProvider SupervisorProviderFunc
    34  	activePlugins      map[string]ActivePlugin
    35  	mutex              sync.RWMutex
    36  }
    37  
    38  type Option func(*Environment)
    39  
    40  // Creates a new environment. At a minimum, the APIProvider and SearchPath options are required.
    41  func New(options ...Option) (*Environment, error) {
    42  	env := &Environment{
    43  		activePlugins: make(map[string]ActivePlugin),
    44  	}
    45  	for _, opt := range options {
    46  		opt(env)
    47  	}
    48  	if env.supervisorProvider == nil {
    49  		env.supervisorProvider = DefaultSupervisorProvider
    50  	}
    51  	if env.searchPath == "" {
    52  		return nil, fmt.Errorf("a search path must be provided")
    53  	}
    54  	return env, nil
    55  }
    56  
    57  // Returns the configured webapp path.
    58  func (env *Environment) WebappPath() string {
    59  	return env.webappPath
    60  }
    61  
    62  // Returns the configured search path.
    63  func (env *Environment) SearchPath() string {
    64  	return env.searchPath
    65  }
    66  
    67  // Returns a list of all plugins found within the environment.
    68  func (env *Environment) Plugins() ([]*model.BundleInfo, error) {
    69  	return ScanSearchPath(env.searchPath)
    70  }
    71  
    72  // Returns a list of all currently active plugins within the environment.
    73  func (env *Environment) ActivePlugins() []*model.BundleInfo {
    74  	env.mutex.RLock()
    75  	defer env.mutex.RUnlock()
    76  
    77  	activePlugins := []*model.BundleInfo{}
    78  	for _, p := range env.activePlugins {
    79  		activePlugins = append(activePlugins, p.BundleInfo)
    80  	}
    81  
    82  	return activePlugins
    83  }
    84  
    85  // Returns the ids of the currently active plugins.
    86  func (env *Environment) ActivePluginIds() (ids []string) {
    87  	env.mutex.RLock()
    88  	defer env.mutex.RUnlock()
    89  
    90  	for id := range env.activePlugins {
    91  		ids = append(ids, id)
    92  	}
    93  	return
    94  }
    95  
    96  // Returns true if the plugin is active, false otherwise.
    97  func (env *Environment) IsPluginActive(pluginId string) bool {
    98  	env.mutex.RLock()
    99  	defer env.mutex.RUnlock()
   100  
   101  	for id := range env.activePlugins {
   102  		if id == pluginId {
   103  			return true
   104  		}
   105  	}
   106  
   107  	return false
   108  }
   109  
   110  // Activates the plugin with the given id.
   111  func (env *Environment) ActivatePlugin(id string) error {
   112  	env.mutex.Lock()
   113  	defer env.mutex.Unlock()
   114  
   115  	if _, ok := env.activePlugins[id]; ok {
   116  		return fmt.Errorf("plugin already active: %v", id)
   117  	}
   118  	plugins, err := ScanSearchPath(env.searchPath)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	var bundle *model.BundleInfo
   123  	for _, p := range plugins {
   124  		if p.Manifest != nil && p.Manifest.Id == id {
   125  			if bundle != nil {
   126  				return fmt.Errorf("multiple plugins found: %v", id)
   127  			}
   128  			bundle = p
   129  		}
   130  	}
   131  	if bundle == nil {
   132  		return fmt.Errorf("plugin not found: %v", id)
   133  	}
   134  
   135  	activePlugin := ActivePlugin{BundleInfo: bundle}
   136  
   137  	var supervisor plugin.Supervisor
   138  
   139  	if bundle.Manifest.Backend != nil {
   140  		if env.apiProvider == nil {
   141  			return fmt.Errorf("env missing api provider, cannot activate plugin: %v", id)
   142  		}
   143  
   144  		supervisor, err = env.supervisorProvider(bundle)
   145  		if err != nil {
   146  			return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id)
   147  		}
   148  		api, err := env.apiProvider(bundle.Manifest)
   149  		if err != nil {
   150  			return errors.Wrapf(err, "unable to get api for plugin: %v", id)
   151  		}
   152  		if err := supervisor.Start(api); err != nil {
   153  			return errors.Wrapf(err, "unable to start plugin: %v", id)
   154  		}
   155  
   156  		activePlugin.Supervisor = supervisor
   157  	}
   158  
   159  	if bundle.Manifest.Webapp != nil {
   160  		if env.webappPath == "" {
   161  			if supervisor != nil {
   162  				supervisor.Stop()
   163  			}
   164  			return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id)
   165  		}
   166  
   167  		bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath)
   168  		if bundlePath == "" || bundlePath[0] == '.' {
   169  			return fmt.Errorf("invalid webapp bundle path")
   170  		}
   171  		bundlePath = filepath.Join(env.searchPath, id, bundlePath)
   172  
   173  		webappBundle, err := ioutil.ReadFile(bundlePath)
   174  		if err != nil {
   175  			// Backwards compatibility for plugins where webapp.bundle_path was ignored. This should
   176  			// be removed eventually.
   177  			if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil {
   178  				webappBundle = webappBundle2
   179  			} else {
   180  				if supervisor != nil {
   181  					supervisor.Stop()
   182  				}
   183  				return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
   184  			}
   185  		}
   186  
   187  		err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644)
   188  		if err != nil {
   189  			if supervisor != nil {
   190  				supervisor.Stop()
   191  			}
   192  			return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
   193  		}
   194  	}
   195  
   196  	env.activePlugins[id] = activePlugin
   197  	return nil
   198  }
   199  
   200  // Deactivates the plugin with the given id.
   201  func (env *Environment) DeactivatePlugin(id string) error {
   202  	env.mutex.Lock()
   203  	defer env.mutex.Unlock()
   204  
   205  	if activePlugin, ok := env.activePlugins[id]; !ok {
   206  		return fmt.Errorf("plugin not active: %v", id)
   207  	} else {
   208  		delete(env.activePlugins, id)
   209  		var err error
   210  		if activePlugin.Supervisor != nil {
   211  			err = activePlugin.Supervisor.Hooks().OnDeactivate()
   212  			if serr := activePlugin.Supervisor.Stop(); err == nil {
   213  				err = serr
   214  			}
   215  		}
   216  		return err
   217  	}
   218  }
   219  
   220  // Deactivates all plugins and gracefully shuts down the environment.
   221  func (env *Environment) Shutdown() (errs []error) {
   222  	env.mutex.Lock()
   223  	defer env.mutex.Unlock()
   224  
   225  	for _, activePlugin := range env.activePlugins {
   226  		if activePlugin.Supervisor != nil {
   227  			if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
   228  				errs = append(errs, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id))
   229  			}
   230  			if err := activePlugin.Supervisor.Stop(); err != nil {
   231  				errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id))
   232  			}
   233  		}
   234  	}
   235  	env.activePlugins = make(map[string]ActivePlugin)
   236  	return
   237  }
   238  
   239  type MultiPluginHooks struct {
   240  	env *Environment
   241  }
   242  
   243  type SinglePluginHooks struct {
   244  	env      *Environment
   245  	pluginId string
   246  }
   247  
   248  func (env *Environment) Hooks() *MultiPluginHooks {
   249  	return &MultiPluginHooks{
   250  		env: env,
   251  	}
   252  }
   253  
   254  func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks {
   255  	return &SinglePluginHooks{
   256  		env:      env,
   257  		pluginId: id,
   258  	}
   259  }
   260  
   261  func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) {
   262  	h.env.mutex.RLock()
   263  	defer h.env.mutex.RUnlock()
   264  
   265  	for _, activePlugin := range h.env.activePlugins {
   266  		if activePlugin.Supervisor == nil {
   267  			continue
   268  		}
   269  		if err := f(activePlugin.Supervisor.Hooks()); err != nil {
   270  			errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
   271  		}
   272  	}
   273  	return
   274  }
   275  
   276  // OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
   277  // encountered will be returned.
   278  func (h *MultiPluginHooks) OnConfigurationChange() []error {
   279  	return h.invoke(func(hooks plugin.Hooks) error {
   280  		if err := hooks.OnConfigurationChange(); err != nil {
   281  			return errors.Wrapf(err, "error calling OnConfigurationChange hook")
   282  		}
   283  		return nil
   284  	})
   285  }
   286  
   287  // ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
   288  // 404 not found.
   289  //
   290  // It expects the request's context to have a plugin_id set.
   291  func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   292  	if id := r.Context().Value("plugin_id"); id != nil {
   293  		if idstr, ok := id.(string); ok {
   294  			h.env.mutex.RLock()
   295  			defer h.env.mutex.RUnlock()
   296  			if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil {
   297  				plugin.Supervisor.Hooks().ServeHTTP(w, r)
   298  				return
   299  			}
   300  		}
   301  	}
   302  	http.NotFound(w, r)
   303  }
   304  
   305  func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error {
   306  	h.env.mutex.RLock()
   307  	defer h.env.mutex.RUnlock()
   308  
   309  	if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil {
   310  		if err := f(activePlugin.Supervisor.Hooks()); err != nil {
   311  			return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id)
   312  		}
   313  		return nil
   314  	}
   315  	return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId)
   316  }
   317  
   318  // ExecuteCommand invokes the ExecuteCommand hook for the plugin.
   319  func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) {
   320  	err = h.invoke(func(hooks plugin.Hooks) error {
   321  		resp, appErr = hooks.ExecuteCommand(args)
   322  		return nil
   323  	})
   324  	return
   325  }