github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+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 !plugin.IsValidId(id) {
   116  		return fmt.Errorf("invalid plugin id: %s", id)
   117  	}
   118  
   119  	if _, ok := env.activePlugins[id]; ok {
   120  		return nil
   121  	}
   122  	plugins, err := ScanSearchPath(env.searchPath)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	var bundle *model.BundleInfo
   127  	for _, p := range plugins {
   128  		if p.Manifest != nil && p.Manifest.Id == id {
   129  			if bundle != nil {
   130  				return fmt.Errorf("multiple plugins found: %v", id)
   131  			}
   132  			bundle = p
   133  		}
   134  	}
   135  	if bundle == nil {
   136  		return fmt.Errorf("plugin not found: %v", id)
   137  	}
   138  
   139  	activePlugin := ActivePlugin{BundleInfo: bundle}
   140  
   141  	var supervisor plugin.Supervisor
   142  
   143  	if bundle.Manifest.Backend != nil {
   144  		if env.apiProvider == nil {
   145  			return fmt.Errorf("env missing api provider, cannot activate plugin: %v", id)
   146  		}
   147  
   148  		supervisor, err = env.supervisorProvider(bundle)
   149  		if err != nil {
   150  			return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id)
   151  		}
   152  		api, err := env.apiProvider(bundle.Manifest)
   153  		if err != nil {
   154  			return errors.Wrapf(err, "unable to get api for plugin: %v", id)
   155  		}
   156  		if err := supervisor.Start(api); err != nil {
   157  			return errors.Wrapf(err, "unable to start plugin: %v", id)
   158  		}
   159  
   160  		activePlugin.Supervisor = supervisor
   161  	}
   162  
   163  	if bundle.Manifest.Webapp != nil {
   164  		if env.webappPath == "" {
   165  			if supervisor != nil {
   166  				supervisor.Stop()
   167  			}
   168  			return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id)
   169  		}
   170  
   171  		bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath)
   172  		if bundlePath == "" || bundlePath[0] == '.' {
   173  			return fmt.Errorf("invalid webapp bundle path")
   174  		}
   175  		bundlePath = filepath.Join(env.searchPath, id, bundlePath)
   176  
   177  		webappBundle, err := ioutil.ReadFile(bundlePath)
   178  		if err != nil {
   179  			// Backwards compatibility for plugins where webapp.bundle_path was ignored. This should
   180  			// be removed eventually.
   181  			if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil {
   182  				webappBundle = webappBundle2
   183  			} else {
   184  				if supervisor != nil {
   185  					supervisor.Stop()
   186  				}
   187  				return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
   188  			}
   189  		}
   190  
   191  		err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644)
   192  		if err != nil {
   193  			if supervisor != nil {
   194  				supervisor.Stop()
   195  			}
   196  			return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
   197  		}
   198  	}
   199  
   200  	env.activePlugins[id] = activePlugin
   201  	return nil
   202  }
   203  
   204  // Deactivates the plugin with the given id.
   205  func (env *Environment) DeactivatePlugin(id string) error {
   206  	env.mutex.Lock()
   207  	defer env.mutex.Unlock()
   208  
   209  	if activePlugin, ok := env.activePlugins[id]; !ok {
   210  		return fmt.Errorf("plugin not active: %v", id)
   211  	} else {
   212  		delete(env.activePlugins, id)
   213  		var err error
   214  		if activePlugin.Supervisor != nil {
   215  			err = activePlugin.Supervisor.Hooks().OnDeactivate()
   216  			if serr := activePlugin.Supervisor.Stop(); err == nil {
   217  				err = serr
   218  			}
   219  		}
   220  		return err
   221  	}
   222  }
   223  
   224  // Deactivates all plugins and gracefully shuts down the environment.
   225  func (env *Environment) Shutdown() (errs []error) {
   226  	env.mutex.Lock()
   227  	defer env.mutex.Unlock()
   228  
   229  	for _, activePlugin := range env.activePlugins {
   230  		if activePlugin.Supervisor != nil {
   231  			if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
   232  				errs = append(errs, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id))
   233  			}
   234  			if err := activePlugin.Supervisor.Stop(); err != nil {
   235  				errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id))
   236  			}
   237  		}
   238  	}
   239  	env.activePlugins = make(map[string]ActivePlugin)
   240  	return
   241  }
   242  
   243  type MultiPluginHooks struct {
   244  	env *Environment
   245  }
   246  
   247  type SinglePluginHooks struct {
   248  	env      *Environment
   249  	pluginId string
   250  }
   251  
   252  func (env *Environment) Hooks() *MultiPluginHooks {
   253  	return &MultiPluginHooks{
   254  		env: env,
   255  	}
   256  }
   257  
   258  func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks {
   259  	return &SinglePluginHooks{
   260  		env:      env,
   261  		pluginId: id,
   262  	}
   263  }
   264  
   265  func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) {
   266  	h.env.mutex.RLock()
   267  	defer h.env.mutex.RUnlock()
   268  
   269  	for _, activePlugin := range h.env.activePlugins {
   270  		if activePlugin.Supervisor == nil {
   271  			continue
   272  		}
   273  		if err := f(activePlugin.Supervisor.Hooks()); err != nil {
   274  			errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
   275  		}
   276  	}
   277  	return
   278  }
   279  
   280  // OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
   281  // encountered will be returned.
   282  func (h *MultiPluginHooks) OnConfigurationChange() []error {
   283  	return h.invoke(func(hooks plugin.Hooks) error {
   284  		if err := hooks.OnConfigurationChange(); err != nil {
   285  			return errors.Wrapf(err, "error calling OnConfigurationChange hook")
   286  		}
   287  		return nil
   288  	})
   289  }
   290  
   291  // ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
   292  // 404 not found.
   293  //
   294  // It expects the request's context to have a plugin_id set.
   295  func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   296  	if id := r.Context().Value("plugin_id"); id != nil {
   297  		if idstr, ok := id.(string); ok {
   298  			h.env.mutex.RLock()
   299  			defer h.env.mutex.RUnlock()
   300  			if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil {
   301  				plugin.Supervisor.Hooks().ServeHTTP(w, r)
   302  				return
   303  			}
   304  		}
   305  	}
   306  	http.NotFound(w, r)
   307  }
   308  
   309  func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error {
   310  	h.env.mutex.RLock()
   311  	defer h.env.mutex.RUnlock()
   312  
   313  	if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil {
   314  		if err := f(activePlugin.Supervisor.Hooks()); err != nil {
   315  			return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id)
   316  		}
   317  		return nil
   318  	}
   319  	return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId)
   320  }
   321  
   322  // ExecuteCommand invokes the ExecuteCommand hook for the plugin.
   323  func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) {
   324  	err = h.invoke(func(hooks plugin.Hooks) error {
   325  		resp, appErr = hooks.ExecuteCommand(args)
   326  		return nil
   327  	})
   328  	return
   329  }