2018-06-25 12:33:13 -07:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
|
|
|
// See LICENSE.txt for license information.
|
|
|
|
|
|
|
|
|
|
package plugin
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2018-07-31 16:29:52 -04:00
|
|
|
"hash/fnv"
|
2018-06-25 12:33:13 -07:00
|
|
|
"io/ioutil"
|
2018-07-31 07:44:44 -07:00
|
|
|
"os"
|
2018-06-25 12:33:13 -07:00
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
2019-06-25 17:44:08 -04:00
|
|
|
"time"
|
2018-06-25 12:33:13 -07:00
|
|
|
|
2021-01-07 22:42:43 +05:30
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
2021-07-22 12:21:47 +05:30
|
|
|
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
|
|
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
|
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
|
|
|
"github.com/mattermost/mattermost-server/v6/utils"
|
2018-06-25 12:33:13 -07:00
|
|
|
)
|
|
|
|
|
|
2019-09-17 16:02:26 -03:00
|
|
|
var ErrNotFound = errors.New("Item not found")
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
type apiImplCreatorFunc func(*model.Manifest) API
|
2018-06-25 12:33:13 -07:00
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
// registeredPlugin stores the state for a given plugin that has been activated
|
|
|
|
|
// or attempted to be activated this server run.
|
|
|
|
|
//
|
|
|
|
|
// If an installed plugin is missing from the env.registeredPlugins map, then the
|
|
|
|
|
// plugin is configured as disabled and has not been activated during this server run.
|
|
|
|
|
type registeredPlugin struct {
|
2018-06-25 12:33:13 -07:00
|
|
|
BundleInfo *model.BundleInfo
|
2020-01-07 17:30:34 +05:30
|
|
|
State int
|
2018-07-13 10:29:50 -04:00
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
supervisor *supervisor
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2020-01-15 13:38:55 -05:00
|
|
|
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
|
|
|
|
|
type PrepackagedPlugin struct {
|
|
|
|
|
Path string
|
|
|
|
|
IconData string
|
|
|
|
|
Manifest *model.Manifest
|
|
|
|
|
Signature []byte
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// 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.
|
2018-06-25 12:33:13 -07:00
|
|
|
type Environment struct {
|
2020-01-15 13:38:55 -05:00
|
|
|
registeredPlugins sync.Map
|
|
|
|
|
pluginHealthCheckJob *PluginHealthCheckJob
|
|
|
|
|
logger *mlog.Logger
|
2020-02-14 15:47:43 -05:00
|
|
|
metrics einterfaces.MetricsInterface
|
2020-01-15 13:38:55 -05:00
|
|
|
newAPIImpl apiImplCreatorFunc
|
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-17 08:53:52 +05:30
|
|
|
dbDriver Driver
|
2020-01-15 13:38:55 -05:00
|
|
|
pluginDir string
|
|
|
|
|
webappPluginDir string
|
|
|
|
|
prepackagedPlugins []*PrepackagedPlugin
|
|
|
|
|
prepackagedPluginsLock sync.RWMutex
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-17 08:53:52 +05:30
|
|
|
func NewEnvironment(newAPIImpl apiImplCreatorFunc,
|
|
|
|
|
dbDriver Driver,
|
|
|
|
|
pluginDir string, webappPluginDir string,
|
|
|
|
|
logger *mlog.Logger,
|
|
|
|
|
metrics einterfaces.MetricsInterface) (*Environment, error) {
|
2018-06-25 12:33:13 -07:00
|
|
|
return &Environment{
|
|
|
|
|
logger: logger,
|
2020-02-14 15:47:43 -05:00
|
|
|
metrics: metrics,
|
2018-06-25 12:33:13 -07:00
|
|
|
newAPIImpl: newAPIImpl,
|
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-17 08:53:52 +05:30
|
|
|
dbDriver: dbDriver,
|
2018-06-25 12:33:13 -07:00
|
|
|
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.
|
2018-07-13 10:29:50 -04:00
|
|
|
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
|
2018-06-25 12:33:13 -07:00
|
|
|
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
|
|
|
|
|
}
|
2020-10-07 10:23:59 +03:00
|
|
|
info := model.BundleInfoForPath(filepath.Join(path, file.Name()))
|
|
|
|
|
if info.Manifest != nil {
|
2018-06-25 12:33:13 -07:00
|
|
|
ret = append(ret, info)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a list of all plugins within the environment.
|
|
|
|
|
func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
2018-07-13 10:29:50 -04:00
|
|
|
return scanSearchPath(env.pluginDir)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2020-01-15 13:38:55 -05:00
|
|
|
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.
|
|
|
|
|
// The list content is immutable and should not be modified.
|
|
|
|
|
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
|
|
|
|
|
env.prepackagedPluginsLock.RLock()
|
|
|
|
|
defer env.prepackagedPluginsLock.RUnlock()
|
|
|
|
|
|
|
|
|
|
return env.prepackagedPlugins
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 12:33:13 -07:00
|
|
|
// Returns a list of all currently active plugins within the environment.
|
2020-01-24 09:00:35 +05:30
|
|
|
// The returned list should not be modified.
|
2018-06-25 12:33:13 -07:00
|
|
|
func (env *Environment) Active() []*model.BundleInfo {
|
|
|
|
|
activePlugins := []*model.BundleInfo{}
|
2019-06-25 17:44:08 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value interface{}) bool {
|
2020-01-24 09:00:35 +05:30
|
|
|
plugin := value.(registeredPlugin)
|
2019-06-25 17:44:08 -04:00
|
|
|
if env.IsActive(plugin.BundleInfo.Manifest.Id) {
|
2019-05-28 10:01:02 -07:00
|
|
|
activePlugins = append(activePlugins, plugin.BundleInfo)
|
|
|
|
|
}
|
2018-07-27 11:37:17 -04:00
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
2018-06-25 12:33:13 -07:00
|
|
|
|
|
|
|
|
return activePlugins
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// IsActive returns true if the plugin with the given id is active.
|
2018-06-25 12:33:13 -07:00
|
|
|
func (env *Environment) IsActive(id string) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
return env.GetPluginState(id) == model.PluginStateRunning
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPluginState returns the current state of a plugin (disabled, running, or error)
|
|
|
|
|
func (env *Environment) GetPluginState(id string) int {
|
|
|
|
|
rp, ok := env.registeredPlugins.Load(id)
|
|
|
|
|
if !ok {
|
|
|
|
|
return model.PluginStateNotRunning
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 09:00:35 +05:30
|
|
|
return rp.(registeredPlugin).State
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
// setPluginState sets the current state of a plugin (disabled, running, or error)
|
|
|
|
|
func (env *Environment) setPluginState(id string, state int) {
|
2019-06-25 17:44:08 -04:00
|
|
|
if rp, ok := env.registeredPlugins.Load(id); ok {
|
2020-01-24 09:00:35 +05:30
|
|
|
p := rp.(registeredPlugin)
|
|
|
|
|
p.State = state
|
|
|
|
|
env.registeredPlugins.Store(id, p)
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2019-04-05 07:35:51 -07:00
|
|
|
// PublicFilesPath returns a path and true if the plugin with the given id is active.
|
|
|
|
|
// It returns an empty string and false if the path is not set or invalid
|
|
|
|
|
func (env *Environment) PublicFilesPath(id string) (string, error) {
|
2020-03-31 20:20:22 -04:00
|
|
|
if !env.IsActive(id) {
|
2019-04-05 07:35:51 -07:00
|
|
|
return "", fmt.Errorf("plugin not found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(env.pluginDir, id, "public"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// Statuses returns a list of plugin statuses representing the state of every plugin
|
2018-06-25 12:33:13 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
pluginState := env.GetPluginState(plugin.Manifest.Id)
|
2018-06-25 12:33:13 -07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-17 16:02:26 -03:00
|
|
|
// GetManifest returns a manifest for a given pluginId.
|
|
|
|
|
// Returns ErrNotFound if plugin is not found.
|
2021-03-23 10:32:54 +01:00
|
|
|
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
|
2019-09-17 16:02:26 -03:00
|
|
|
plugins, err := env.Available()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, plugin := range plugins {
|
2021-03-23 10:32:54 +01:00
|
|
|
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
|
2019-09-17 16:02:26 -03:00
|
|
|
return plugin.Manifest, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-31 16:29:52 -04:00
|
|
|
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
|
2018-06-25 12:33:13 -07:00
|
|
|
// Check if we are already active
|
2019-06-25 17:44:08 -04:00
|
|
|
if env.IsActive(id) {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, nil
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plugins, err := env.Available()
|
|
|
|
|
if err != nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, err
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
var pluginInfo *model.BundleInfo
|
|
|
|
|
for _, p := range plugins {
|
|
|
|
|
if p.Manifest != nil && p.Manifest.Id == id {
|
|
|
|
|
if pluginInfo != nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
pluginInfo = p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if pluginInfo == nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, fmt.Errorf("plugin not found: %v", id)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
rp := newRegisteredPlugin(pluginInfo)
|
2020-01-24 09:00:35 +05:30
|
|
|
env.registeredPlugins.Store(id, rp)
|
2019-06-25 17:44:08 -04:00
|
|
|
|
2018-06-25 12:33:13 -07:00
|
|
|
defer func() {
|
|
|
|
|
if reterr == nil {
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateRunning)
|
2018-06-25 12:33:13 -07:00
|
|
|
} else {
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateFailedToStart)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2018-11-05 14:29:25 +01:00
|
|
|
if pluginInfo.Manifest.MinServerVersion != "" {
|
|
|
|
|
fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, fmt.Errorf("%v: %v", err.Error(), id)
|
|
|
|
|
}
|
|
|
|
|
if !fulfilled {
|
|
|
|
|
return nil, false, fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-21 11:07:32 -04:00
|
|
|
componentActivated := false
|
|
|
|
|
|
|
|
|
|
if pluginInfo.Manifest.HasWebapp() {
|
2019-08-22 15:17:47 -04:00
|
|
|
updatedManifest, err := env.UnpackWebappBundle(id)
|
2018-07-31 16:29:52 -04:00
|
|
|
if err != nil {
|
2019-08-22 15:17:47 -04:00
|
|
|
return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2019-08-22 15:17:47 -04:00
|
|
|
pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash
|
2018-09-21 11:07:32 -04:00
|
|
|
|
|
|
|
|
componentActivated = true
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2018-07-18 18:32:33 -04:00
|
|
|
if pluginInfo.Manifest.HasServer() {
|
DB driver implementation via RPC (#17779)
This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer.
To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins.
A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id.
When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data.
Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned.
Some pending things:
- Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it.
- RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface.
- Master/Replica API support.
2021-06-17 08:53:52 +05:30
|
|
|
sup, err := newSupervisor(pluginInfo, env.newAPIImpl(pluginInfo.Manifest), env.dbDriver, env.logger, env.metrics)
|
2018-06-25 12:33:13 -07:00
|
|
|
if err != nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id)
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2020-01-24 09:49:49 -05:00
|
|
|
|
|
|
|
|
if err := sup.Hooks().OnActivate(); err != nil {
|
|
|
|
|
sup.Shutdown()
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
2019-06-25 17:44:08 -04:00
|
|
|
rp.supervisor = sup
|
2020-01-24 09:00:35 +05:30
|
|
|
env.registeredPlugins.Store(id, rp)
|
2018-09-21 11:07:32 -04:00
|
|
|
|
|
|
|
|
componentActivated = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !componentActivated {
|
|
|
|
|
return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component")
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2018-07-31 16:29:52 -04:00
|
|
|
return pluginInfo.Manifest, true, nil
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
func (env *Environment) RemovePlugin(id string) {
|
|
|
|
|
if _, ok := env.registeredPlugins.Load(id); ok {
|
|
|
|
|
env.registeredPlugins.Delete(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 12:33:13 -07:00
|
|
|
// Deactivates the plugin with the given id.
|
2018-07-31 16:29:52 -04:00
|
|
|
func (env *Environment) Deactivate(id string) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
p, ok := env.registeredPlugins.Load(id)
|
2018-07-27 11:37:17 -04:00
|
|
|
if !ok {
|
2018-07-31 16:29:52 -04:00
|
|
|
return false
|
2018-07-27 11:37:17 -04:00
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
isActive := env.IsActive(id)
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateNotRunning)
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
if !isActive {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 09:00:35 +05:30
|
|
|
rp := p.(registeredPlugin)
|
2019-06-25 17:44:08 -04:00
|
|
|
if rp.supervisor != nil {
|
|
|
|
|
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
|
|
|
|
|
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2019-06-25 17:44:08 -04:00
|
|
|
rp.supervisor.Shutdown()
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2018-07-31 16:29:52 -04:00
|
|
|
|
|
|
|
|
return true
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2019-05-09 16:08:31 -04:00
|
|
|
// RestartPlugin deactivates, then activates the plugin with the given id.
|
|
|
|
|
func (env *Environment) RestartPlugin(id string) error {
|
|
|
|
|
env.Deactivate(id)
|
|
|
|
|
_, _, err := env.Activate(id)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// Shutdown deactivates all plugins and gracefully shuts down the environment.
|
2018-06-25 12:33:13 -07:00
|
|
|
func (env *Environment) Shutdown() {
|
2019-08-29 20:36:38 +02:00
|
|
|
if env.pluginHealthCheckJob != nil {
|
|
|
|
|
env.pluginHealthCheckJob.Cancel()
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 17:27:36 -03:00
|
|
|
var wg sync.WaitGroup
|
2019-06-25 17:44:08 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value interface{}) bool {
|
2020-01-24 09:00:35 +05:30
|
|
|
rp := value.(registeredPlugin)
|
2018-06-25 12:33:13 -07:00
|
|
|
|
2020-01-24 09:49:49 -05:00
|
|
|
if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
2019-09-05 17:27:36 -03:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
|
|
|
|
|
done := make(chan bool)
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
2019-06-25 17:44:08 -04:00
|
|
|
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
|
|
|
|
|
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2019-09-05 17:27:36 -03:00
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-time.After(10 * time.Second):
|
|
|
|
|
env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id))
|
|
|
|
|
case <-done:
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
rp.supervisor.Shutdown()
|
2019-09-05 17:27:36 -03:00
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2019-09-05 17:27:36 -03:00
|
|
|
env.registeredPlugins.Range(func(key, value interface{}) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
env.registeredPlugins.Delete(key)
|
2018-07-27 11:37:17 -04:00
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 15:17:47 -04:00
|
|
|
// UnpackWebappBundle unpacks webapp bundle for a given plugin id on disk.
|
|
|
|
|
func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) {
|
|
|
|
|
plugins, err := env.Available()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.New("Unable to get available plugins")
|
|
|
|
|
}
|
|
|
|
|
var manifest *model.Manifest
|
|
|
|
|
for _, p := range plugins {
|
|
|
|
|
if p.Manifest != nil && p.Manifest.Id == id {
|
|
|
|
|
if manifest != nil {
|
|
|
|
|
return nil, fmt.Errorf("multiple plugins found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
manifest = p.Manifest
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if manifest == nil {
|
|
|
|
|
return nil, fmt.Errorf("plugin not found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bundlePath := filepath.Clean(manifest.Webapp.BundlePath)
|
|
|
|
|
if bundlePath == "" || bundlePath[0] == '.' {
|
|
|
|
|
return nil, 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, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil {
|
|
|
|
|
return nil, 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, errors.Wrapf(err, "unable to read webapp bundle: %v", id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hash := fnv.New64a()
|
|
|
|
|
if _, err = hash.Write(sourceBundleFileContents); err != nil {
|
|
|
|
|
return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id)
|
|
|
|
|
}
|
|
|
|
|
manifest.Webapp.BundleHash = hash.Sum([]byte{})
|
|
|
|
|
|
|
|
|
|
if err = os.Rename(
|
|
|
|
|
sourceBundleFilepath,
|
|
|
|
|
filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, manifest.Webapp.BundleHash)),
|
|
|
|
|
); err != nil {
|
|
|
|
|
return nil, errors.Wrapf(err, "unable to rename webapp bundle: %v", id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return manifest, nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// HooksForPlugin returns the hooks API for the plugin with the given id.
|
|
|
|
|
//
|
|
|
|
|
// Consider using RunMultiPluginHook instead.
|
2018-06-25 12:33:13 -07:00
|
|
|
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
|
2019-06-25 17:44:08 -04:00
|
|
|
if p, ok := env.registeredPlugins.Load(id); ok {
|
2020-01-24 09:00:35 +05:30
|
|
|
rp := p.(registeredPlugin)
|
2020-01-24 09:49:49 -05:00
|
|
|
if rp.supervisor != nil && env.IsActive(id) {
|
2019-06-25 17:44:08 -04:00
|
|
|
return rp.supervisor.Hooks(), nil
|
2018-07-27 11:37:17 -04:00
|
|
|
}
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("plugin not found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 09:49:49 -05:00
|
|
|
// RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId.
|
2018-07-13 10:29:50 -04:00
|
|
|
//
|
|
|
|
|
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
|
|
|
|
|
// plugins is not specified.
|
2021-03-23 10:32:54 +01:00
|
|
|
func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool, hookId int) {
|
2020-02-14 15:47:43 -05:00
|
|
|
startTime := time.Now()
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value interface{}) bool {
|
2020-01-24 09:00:35 +05:30
|
|
|
rp := value.(registeredPlugin)
|
2018-06-25 12:33:13 -07:00
|
|
|
|
2021-03-23 10:32:54 +01:00
|
|
|
if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
2018-07-27 11:37:17 -04:00
|
|
|
return true
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2020-02-14 15:47:43 -05:00
|
|
|
hookStartTime := time.Now()
|
|
|
|
|
result := hookRunnerFunc(rp.supervisor.Hooks())
|
|
|
|
|
|
|
|
|
|
if env.metrics != nil {
|
|
|
|
|
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
|
|
|
|
|
env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
2018-07-27 11:37:17 -04:00
|
|
|
})
|
2020-02-14 15:47:43 -05:00
|
|
|
|
|
|
|
|
if env.metrics != nil {
|
|
|
|
|
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
|
|
|
|
env.metrics.ObservePluginMultiHookDuration(elapsedTime)
|
|
|
|
|
}
|
2018-06-25 12:33:13 -07:00
|
|
|
}
|
2019-06-25 17:44:08 -04:00
|
|
|
|
2020-10-02 03:02:58 -05:00
|
|
|
// PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed.
|
|
|
|
|
func (env *Environment) PerformHealthCheck(id string) error {
|
2020-03-31 20:20:22 -04:00
|
|
|
p, ok := env.registeredPlugins.Load(id)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
rp := p.(registeredPlugin)
|
|
|
|
|
|
|
|
|
|
sup := rp.supervisor
|
|
|
|
|
if sup == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return sup.PerformHealthCheck()
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-15 13:38:55 -05:00
|
|
|
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
|
|
|
|
|
func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) {
|
|
|
|
|
env.prepackagedPluginsLock.Lock()
|
|
|
|
|
env.prepackagedPlugins = plugins
|
|
|
|
|
env.prepackagedPluginsLock.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 09:00:35 +05:30
|
|
|
func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin {
|
2019-06-25 17:44:08 -04:00
|
|
|
state := model.PluginStateNotRunning
|
2020-03-31 20:20:22 -04:00
|
|
|
return registeredPlugin{State: state, BundleInfo: bundle}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InitPluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled.
|
|
|
|
|
func (env *Environment) InitPluginHealthCheckJob(enable bool) {
|
|
|
|
|
// Config is set to enable. No job exists, start a new job.
|
|
|
|
|
if enable && env.pluginHealthCheckJob == nil {
|
2021-01-04 11:32:29 +05:30
|
|
|
mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval))
|
2020-03-31 20:20:22 -04:00
|
|
|
|
|
|
|
|
job := newPluginHealthCheckJob(env)
|
|
|
|
|
env.pluginHealthCheckJob = job
|
|
|
|
|
go job.run()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Config is set to disable. Job exists, kill existing job.
|
|
|
|
|
if !enable && env.pluginHealthCheckJob != nil {
|
|
|
|
|
mlog.Debug("Disabling plugin health check job")
|
|
|
|
|
|
|
|
|
|
env.pluginHealthCheckJob.Cancel()
|
|
|
|
|
env.pluginHealthCheckJob = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPluginHealthCheckJob returns the configured PluginHealthCheckJob, if any.
|
|
|
|
|
func (env *Environment) GetPluginHealthCheckJob() *PluginHealthCheckJob {
|
|
|
|
|
return env.pluginHealthCheckJob
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|