Files
mattermost/plugin/environment.go
Daniel Schalla 531897b1f0 add megacheck as makefile target (#9288)
Fix code issues in channel_test.go

Fix Channel Test Issues detected by Megacheck

Fix API Emoji Test Issues detected by Megacheck

Fixed API Issues Reported by Megacheck

Fixed App issues reported by megacheck

Remaining fixes

removed test added by mistake from old HEAD

gofmt

Store Fixes

simplified returns

Fix test for multi member channel delete

revert to delete unused function
2018-09-03 14:08:40 +02:00

285 lines
8.3 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"fmt"
"hash/fnv"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/pkg/errors"
)
type apiImplCreatorFunc func(*model.Manifest) API
// multiPluginHookRunnerFunc is a callback function to invoke as part of RunMultiPluginHook.
//
// Return false to stop the hook from iterating to subsequent plugins.
type multiPluginHookRunnerFunc func(hooks Hooks) bool
type activePlugin struct {
BundleInfo *model.BundleInfo
State int
supervisor *supervisor
}
// Environment represents the execution environment of active plugins.
//
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
// of active plugins.
type Environment struct {
activePlugins sync.Map
logger *mlog.Logger
newAPIImpl apiImplCreatorFunc
pluginDir string
webappPluginDir string
}
func NewEnvironment(newAPIImpl apiImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) {
return &Environment{
logger: logger,
newAPIImpl: newAPIImpl,
pluginDir: pluginDir,
webappPluginDir: webappPluginDir,
}, nil
}
// Performs a full scan of the given path.
//
// This function will return info for all subdirectories that appear to be plugins (i.e. all
// subdirectories containing plugin manifest files, regardless of whether they could actually be
// parsed).
//
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
var ret []*model.BundleInfo
for _, file := range files {
if !file.IsDir() || file.Name()[0] == '.' {
continue
}
if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" {
ret = append(ret, info)
}
}
return ret, nil
}
// Returns a list of all plugins within the environment.
func (env *Environment) Available() ([]*model.BundleInfo, error) {
return scanSearchPath(env.pluginDir)
}
// Returns a list of all currently active plugins within the environment.
func (env *Environment) Active() []*model.BundleInfo {
activePlugins := []*model.BundleInfo{}
env.activePlugins.Range(func(key, value interface{}) bool {
activePlugins = append(activePlugins, value.(activePlugin).BundleInfo)
return true
})
return activePlugins
}
// IsActive returns true if the plugin with the given id is active.
func (env *Environment) IsActive(id string) bool {
_, ok := env.activePlugins.Load(id)
return ok
}
// Statuses returns a list of plugin statuses representing the state of every plugin
func (env *Environment) Statuses() (model.PluginStatuses, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.Wrap(err, "unable to get plugin statuses")
}
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
for _, plugin := range plugins {
// For now we don't handle bad manifests, we should
if plugin.Manifest == nil {
continue
}
pluginState := model.PluginStateNotRunning
if plugin, ok := env.activePlugins.Load(plugin.Manifest.Id); ok {
pluginState = plugin.(activePlugin).State
}
status := &model.PluginStatus{
PluginId: plugin.Manifest.Id,
PluginPath: filepath.Dir(plugin.ManifestPath),
State: pluginState,
Name: plugin.Manifest.Name,
Description: plugin.Manifest.Description,
Version: plugin.Manifest.Version,
}
pluginStatuses = append(pluginStatuses, status)
}
return pluginStatuses, nil
}
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
// Check if we are already active
if _, ok := env.activePlugins.Load(id); ok {
return nil, false, nil
}
plugins, err := env.Available()
if err != nil {
return nil, false, err
}
var pluginInfo *model.BundleInfo
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
if pluginInfo != nil {
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
}
pluginInfo = p
}
}
if pluginInfo == nil {
return nil, false, fmt.Errorf("plugin not found: %v", id)
}
activePlugin := activePlugin{BundleInfo: pluginInfo}
defer func() {
if reterr == nil {
activePlugin.State = model.PluginStateRunning
} else {
activePlugin.State = model.PluginStateFailedToStart
}
env.activePlugins.Store(pluginInfo.Manifest.Id, activePlugin)
}()
if pluginInfo.Manifest.Webapp != nil {
bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath)
if bundlePath == "" || bundlePath[0] == '.' {
return nil, false, fmt.Errorf("invalid webapp bundle path")
}
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
destinationPath := filepath.Join(env.webappPluginDir, id)
if err := os.RemoveAll(destinationPath); err != nil {
return nil, false, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath)
}
if err := utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil {
return nil, false, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id)
}
sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath))
sourceBundleFileContents, err := ioutil.ReadFile(sourceBundleFilepath)
if err != nil {
return nil, false, errors.Wrapf(err, "unable to read webapp bundle: %v", id)
}
hash := fnv.New64a()
hash.Write(sourceBundleFileContents)
pluginInfo.Manifest.Webapp.BundleHash = hash.Sum([]byte{})
if err := os.Rename(
sourceBundleFilepath,
filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, pluginInfo.Manifest.Webapp.BundleHash)),
); err != nil {
return nil, false, errors.Wrapf(err, "unable to rename webapp bundle: %v", id)
}
}
if pluginInfo.Manifest.HasServer() {
supervisor, err := newSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest))
if err != nil {
return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id)
}
activePlugin.supervisor = supervisor
}
return pluginInfo.Manifest, true, nil
}
// Deactivates the plugin with the given id.
func (env *Environment) Deactivate(id string) bool {
p, ok := env.activePlugins.Load(id)
if !ok {
return false
}
env.activePlugins.Delete(id)
activePlugin := p.(activePlugin)
if activePlugin.supervisor != nil {
if err := activePlugin.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
}
activePlugin.supervisor.Shutdown()
}
return true
}
// Shutdown deactivates all plugins and gracefully shuts down the environment.
func (env *Environment) Shutdown() {
env.activePlugins.Range(func(key, value interface{}) bool {
activePlugin := value.(activePlugin)
if activePlugin.supervisor != nil {
if err := activePlugin.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
}
activePlugin.supervisor.Shutdown()
}
env.activePlugins.Delete(key)
return true
})
}
// HooksForPlugin returns the hooks API for the plugin with the given id.
//
// Consider using RunMultiPluginHook instead.
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
if p, ok := env.activePlugins.Load(id); ok {
activePlugin := p.(activePlugin)
if activePlugin.supervisor != nil {
return activePlugin.supervisor.Hooks(), nil
}
}
return nil, fmt.Errorf("plugin not found: %v", id)
}
// RunMultiPluginHook invokes hookRunnerFunc for each plugin that implements the given hookId.
//
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
// plugins is not specified.
func (env *Environment) RunMultiPluginHook(hookRunnerFunc multiPluginHookRunnerFunc, hookId int) {
env.activePlugins.Range(func(key, value interface{}) bool {
activePlugin := value.(activePlugin)
if activePlugin.supervisor == nil || !activePlugin.supervisor.Implements(hookId) {
return true
}
if !hookRunnerFunc(activePlugin.supervisor.Hooks()) {
return false
}
return true
})
}