summaryrefslogtreecommitdiffstats
path: root/plugin
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2018-06-25 12:33:13 -0700
committerGitHub <noreply@github.com>2018-06-25 12:33:13 -0700
commit1e5c432e1029601a664454388ae366ef69618d62 (patch)
treecb9e8bfb66640ac3b29c934bb2c3202d25aeb368 /plugin
parentecefa6cdd1e7376046bbec82c1b47f7756fea646 (diff)
downloadchat-1e5c432e1029601a664454388ae366ef69618d62.tar.gz
chat-1e5c432e1029601a664454388ae366ef69618d62.tar.bz2
chat-1e5c432e1029601a664454388ae366ef69618d62.zip
MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)
* Moving plugins to use hashicorp go-plugin. * Tweaks from feedback.
Diffstat (limited to 'plugin')
-rw-r--r--plugin/api.go18
-rw-r--r--plugin/client.go51
-rw-r--r--plugin/client_rpc.go331
-rw-r--r--plugin/client_rpc_generated.go1103
-rw-r--r--plugin/environment.go260
-rw-r--r--plugin/example_hello_user_test.go39
-rw-r--r--plugin/example_hello_world_test.go20
-rw-r--r--plugin/example_test.go35
-rw-r--r--plugin/hclog_adapter.go73
-rw-r--r--plugin/hooks.go27
-rw-r--r--plugin/http.go91
-rw-r--r--plugin/interface_generator/main.go377
-rw-r--r--plugin/io_rpc.go (renamed from plugin/rpcplugin/io.go)4
-rw-r--r--plugin/plugin.go18
-rw-r--r--plugin/pluginenv/environment.go396
-rw-r--r--plugin/pluginenv/environment_test.go409
-rw-r--r--plugin/pluginenv/options.go50
-rw-r--r--plugin/pluginenv/options_test.go32
-rw-r--r--plugin/pluginenv/search_path.go35
-rw-r--r--plugin/pluginenv/search_path_test.go62
-rw-r--r--plugin/plugintest/api.go120
-rw-r--r--plugin/plugintest/apioverride.go18
-rw-r--r--plugin/plugintest/hooks.go40
-rw-r--r--plugin/plugintest/key_value_store.go70
-rw-r--r--plugin/rpcplugin/api.go718
-rw-r--r--plugin/rpcplugin/api_test.go300
-rw-r--r--plugin/rpcplugin/hooks.go398
-rw-r--r--plugin/rpcplugin/hooks_test.go237
-rw-r--r--plugin/rpcplugin/http.go91
-rw-r--r--plugin/rpcplugin/http_test.go61
-rw-r--r--plugin/rpcplugin/ipc.go31
-rw-r--r--plugin/rpcplugin/ipc_test.go63
-rw-r--r--plugin/rpcplugin/main.go47
-rw-r--r--plugin/rpcplugin/main_test.go63
-rw-r--r--plugin/rpcplugin/muxer.go264
-rw-r--r--plugin/rpcplugin/muxer_test.go197
-rw-r--r--plugin/rpcplugin/process.go26
-rw-r--r--plugin/rpcplugin/process_test.go60
-rw-r--r--plugin/rpcplugin/process_unix.go48
-rw-r--r--plugin/rpcplugin/process_windows.go648
-rw-r--r--plugin/rpcplugin/rpcplugintest/rpcplugintest.go26
-rw-r--r--plugin/rpcplugin/rpcplugintest/supervisor.go312
-rw-r--r--plugin/rpcplugin/sandbox/main_test.go18
-rw-r--r--plugin/rpcplugin/sandbox/sandbox.go34
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_linux.go488
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_linux_test.go159
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_other.go22
-rw-r--r--plugin/rpcplugin/sandbox/sandbox_test.go25
-rw-r--r--plugin/rpcplugin/sandbox/seccomp_linux.go178
-rw-r--r--plugin/rpcplugin/sandbox/seccomp_linux_amd64.go301
-rw-r--r--plugin/rpcplugin/sandbox/seccomp_linux_other.go10
-rw-r--r--plugin/rpcplugin/sandbox/seccomp_linux_test.go210
-rw-r--r--plugin/rpcplugin/sandbox/supervisor.go33
-rw-r--r--plugin/rpcplugin/sandbox/supervisor_test.go18
-rw-r--r--plugin/rpcplugin/supervisor.go176
-rw-r--r--plugin/rpcplugin/supervisor_test.go14
-rw-r--r--plugin/supervisor.go102
-rw-r--r--plugin/supervisor_test.go148
58 files changed, 2673 insertions, 6532 deletions
diff --git a/plugin/api.go b/plugin/api.go
index d62c2f069..ed2bfa733 100644
--- a/plugin/api.go
+++ b/plugin/api.go
@@ -4,6 +4,7 @@
package plugin
import (
+ "github.com/hashicorp/go-plugin"
"github.com/mattermost/mattermost-server/model"
)
@@ -104,17 +105,18 @@ type API interface {
// UpdatePost updates a post.
UpdatePost(post *model.Post) (*model.Post, *model.AppError)
- // KeyValueStore returns an object for accessing the persistent key value storage.
- KeyValueStore() KeyValueStore
-}
-
-type KeyValueStore interface {
// Set will store a key-value pair, unique per plugin.
- Set(key string, value []byte) *model.AppError
+ KVSet(key string, value []byte) *model.AppError
// Get will retrieve a value based on the key. Returns nil for non-existent keys.
- Get(key string) ([]byte, *model.AppError)
+ KVGet(key string) ([]byte, *model.AppError)
// Delete will remove a key-value pair. Returns nil for non-existent keys.
- Delete(key string) *model.AppError
+ KVDelete(key string) *model.AppError
+}
+
+var Handshake = plugin.HandshakeConfig{
+ ProtocolVersion: 1,
+ MagicCookieKey: "MATTERMOST_PLUGIN",
+ MagicCookieValue: "Securely message teams, anywhere.",
}
diff --git a/plugin/client.go b/plugin/client.go
new file mode 100644
index 000000000..3f6fbc7a6
--- /dev/null
+++ b/plugin/client.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package plugin
+
+import (
+ "github.com/hashicorp/go-plugin"
+)
+
+// Starts the serving of a Mattermost plugin over rpc or gRPC
+// Call this when your plugin is ready to start
+func ClientMain(pluginImplementation interface{}) {
+ if impl, ok := pluginImplementation.(interface {
+ SetAPI(api API)
+ SetSelfRef(ref interface{})
+ }); !ok {
+ panic("Plugin implementation given must embed plugin.MattermostPlugin")
+ } else {
+ impl.SetAPI(nil)
+ impl.SetSelfRef(pluginImplementation)
+ }
+
+ pluginMap := map[string]plugin.Plugin{
+ "hooks": &HooksPlugin{hooks: pluginImplementation},
+ }
+
+ plugin.Serve(&plugin.ServeConfig{
+ HandshakeConfig: Handshake,
+ Plugins: pluginMap,
+ })
+}
+
+type MattermostPlugin struct {
+ API API
+ selfRef interface{} // This is so we can unmarshal into our parent
+}
+
+func (p *MattermostPlugin) SetAPI(api API) {
+ p.API = api
+}
+
+func (p *MattermostPlugin) SetSelfRef(ref interface{}) {
+ p.selfRef = ref
+}
+
+func (p *MattermostPlugin) OnConfigurationChange() error {
+ if p.selfRef != nil {
+ return p.API.LoadPluginConfiguration(p.selfRef)
+ }
+ return nil
+}
diff --git a/plugin/client_rpc.go b/plugin/client_rpc.go
new file mode 100644
index 000000000..159d41201
--- /dev/null
+++ b/plugin/client_rpc.go
@@ -0,0 +1,331 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+//go:generate go run interface_generator/main.go
+
+package plugin
+
+import (
+ "bytes"
+ "encoding/gob"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/rpc"
+ "reflect"
+
+ "github.com/hashicorp/go-plugin"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+var HookNameToId map[string]int = make(map[string]int)
+
+type HooksRPCClient struct {
+ client *rpc.Client
+ log *mlog.Logger
+ muxBroker *plugin.MuxBroker
+ apiImpl API
+ implemented [TotalHooksId]bool
+}
+
+type HooksRPCServer struct {
+ impl interface{}
+ muxBroker *plugin.MuxBroker
+ apiRPCClient *APIRPCClient
+ log *mlog.Logger
+}
+
+// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin
+type HooksPlugin struct {
+ hooks interface{}
+ apiImpl API
+ log *mlog.Logger
+}
+
+func (p *HooksPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
+ return &HooksRPCServer{impl: p.hooks, muxBroker: b}, nil
+}
+
+func (p *HooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (interface{}, error) {
+ return &HooksRPCClient{client: client, log: p.log, muxBroker: b, apiImpl: p.apiImpl}, nil
+}
+
+type APIRPCClient struct {
+ client *rpc.Client
+ log *mlog.Logger
+}
+
+type APIRPCServer struct {
+ impl API
+}
+
+// Registering some types used by MM for encoding/gob used by rpc
+func init() {
+ gob.Register([]*model.SlackAttachment{})
+ gob.Register([]interface{}{})
+ gob.Register(map[string]interface{}{})
+}
+
+// These enforce compile time checks to make sure types implement the interface
+// If you are getting an error here, you probably need to run `make pluginapi` to
+// autogenerate RPC glue code
+var _ plugin.Plugin = &HooksPlugin{}
+var _ Hooks = &HooksRPCClient{}
+
+//
+// Below are specal cases for hooks or APIs that can not be auto generated
+//
+
+func (g *HooksRPCClient) Implemented() (impl []string, err error) {
+ err = g.client.Call("Plugin.Implemented", struct{}{}, &impl)
+ for _, hookName := range impl {
+ if hookId, ok := HookNameToId[hookName]; ok {
+ g.implemented[hookId] = true
+ }
+ }
+ return
+}
+
+// Implemented replies with the names of the hooks that are implemented.
+func (s *HooksRPCServer) Implemented(args struct{}, reply *[]string) error {
+ ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem()
+ implType := reflect.TypeOf(s.impl)
+ selfType := reflect.TypeOf(s)
+ var methods []string
+ for i := 0; i < ifaceType.NumMethod(); i++ {
+ method := ifaceType.Method(i)
+ if m, ok := implType.MethodByName(method.Name); !ok {
+ continue
+ } else if m.Type.NumIn() != method.Type.NumIn()+1 {
+ continue
+ } else if m.Type.NumOut() != method.Type.NumOut() {
+ continue
+ } else {
+ match := true
+ for j := 0; j < method.Type.NumIn(); j++ {
+ if m.Type.In(j+1) != method.Type.In(j) {
+ match = false
+ break
+ }
+ }
+ for j := 0; j < method.Type.NumOut(); j++ {
+ if m.Type.Out(j) != method.Type.Out(j) {
+ match = false
+ break
+ }
+ }
+ if !match {
+ continue
+ }
+ }
+ if _, ok := selfType.MethodByName(method.Name); !ok {
+ continue
+ }
+ methods = append(methods, method.Name)
+ }
+ *reply = methods
+ return nil
+}
+
+type OnActivateArgs struct {
+ APIMuxId uint32
+}
+
+type OnActivateReturns struct {
+ A error
+}
+
+func (g *HooksRPCClient) OnActivate() error {
+ muxId := g.muxBroker.NextId()
+ go g.muxBroker.AcceptAndServe(muxId, &APIRPCServer{
+ impl: g.apiImpl,
+ })
+
+ _args := &OnActivateArgs{
+ APIMuxId: muxId,
+ }
+ _returns := &OnActivateReturns{}
+
+ if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil {
+ g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *HooksRPCServer) OnActivate(args *OnActivateArgs, returns *OnActivateReturns) error {
+ connection, err := s.muxBroker.Dial(args.APIMuxId)
+ if err != nil {
+ return err // Where does this go?
+ }
+
+ // Settings for this should come from the parent process, for now just set it up
+ // though stdout.
+ logger := mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: mlog.LevelDebug,
+ EnableFile: false,
+ })
+ logger = logger.With(mlog.Bool("plugin_subprocess", true))
+
+ s.log = logger
+
+ s.apiRPCClient = &APIRPCClient{
+ client: rpc.NewClient(connection),
+ log: logger,
+ }
+
+ if mmplugin, ok := s.impl.(interface {
+ SetAPI(api API)
+ OnConfigurationChange() error
+ }); !ok {
+ } else {
+ mmplugin.SetAPI(s.apiRPCClient)
+ mmplugin.OnConfigurationChange()
+ }
+
+ if hook, ok := s.impl.(interface {
+ OnActivate() error
+ }); ok {
+ returns.A = hook.OnActivate()
+ }
+ return nil
+}
+
+type LoadPluginConfigurationArgs struct {
+}
+
+type LoadPluginConfigurationReturns struct {
+ A []byte
+}
+
+func (g *APIRPCClient) LoadPluginConfiguration(dest interface{}) error {
+ _args := &LoadPluginConfigurationArgs{}
+ _returns := &LoadPluginConfigurationReturns{}
+ if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil {
+ g.log.Error("RPC call to LoadPluginConfiguration API failed.", mlog.Err(err))
+ }
+ return json.Unmarshal(_returns.A, dest)
+}
+
+func (s *APIRPCServer) LoadPluginConfiguration(args *LoadPluginConfigurationArgs, returns *LoadPluginConfigurationReturns) error {
+ var config interface{}
+ if hook, ok := s.impl.(interface {
+ LoadPluginConfiguration(dest interface{}) error
+ }); ok {
+ if err := hook.LoadPluginConfiguration(&config); err != nil {
+ return err
+ }
+ }
+ b, err := json.Marshal(config)
+ if err != nil {
+ return err
+ }
+ returns.A = b
+ return nil
+}
+
+func init() {
+ HookNameToId["ServeHTTP"] = ServeHTTPId
+}
+
+type ServeHTTPArgs struct {
+ ResponseWriterStream uint32
+ Request *http.Request
+ RequestBodyStream uint32
+}
+
+func (g *HooksRPCClient) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !g.implemented[ServeHTTPId] {
+ http.NotFound(w, r)
+ return
+ }
+
+ serveHTTPStreamId := g.muxBroker.NextId()
+ go func() {
+ connection, err := g.muxBroker.Accept(serveHTTPStreamId)
+ if err != nil {
+ g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err))
+ http.Error(w, "500 internal server error", http.StatusInternalServerError)
+ return
+ }
+ defer connection.Close()
+
+ rpcServer := rpc.NewServer()
+ if err := rpcServer.RegisterName("Plugin", &HTTPResponseWriterRPCServer{w: w}); err != nil {
+ g.log.Error("Plugin failed to ServeHTTP, coulden't register RPC name", mlog.Err(err))
+ http.Error(w, "500 internal server error", http.StatusInternalServerError)
+ return
+ }
+ rpcServer.ServeConn(connection)
+ }()
+
+ requestBodyStreamId := uint32(0)
+ if r.Body != nil {
+ requestBodyStreamId = g.muxBroker.NextId()
+ go func() {
+ bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
+ if err != nil {
+ g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connecion", mlog.Err(err))
+ http.Error(w, "500 internal server error", http.StatusInternalServerError)
+ return
+ }
+ defer bodyConnection.Close()
+ ServeIOReader(r.Body, bodyConnection)
+ }()
+ }
+
+ forwardedRequest := &http.Request{
+ Method: r.Method,
+ URL: r.URL,
+ Proto: r.Proto,
+ ProtoMajor: r.ProtoMajor,
+ ProtoMinor: r.ProtoMinor,
+ Header: r.Header,
+ Host: r.Host,
+ RemoteAddr: r.RemoteAddr,
+ RequestURI: r.RequestURI,
+ }
+
+ if err := g.client.Call("Plugin.ServeHTTP", ServeHTTPArgs{
+ ResponseWriterStream: serveHTTPStreamId,
+ Request: forwardedRequest,
+ RequestBodyStream: requestBodyStreamId,
+ }, nil); err != nil {
+ mlog.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err))
+ http.Error(w, "500 internal server error", http.StatusInternalServerError)
+ }
+ return
+}
+
+func (s *HooksRPCServer) ServeHTTP(args *ServeHTTPArgs, returns *struct{}) error {
+ connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
+ if err != nil {
+ s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err))
+ return err
+ }
+ w := ConnectHTTPResponseWriter(connection)
+ defer w.Close()
+
+ r := args.Request
+ if args.RequestBodyStream != 0 {
+ connection, err := s.muxBroker.Dial(args.RequestBodyStream)
+ if err != nil {
+ s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err))
+ return err
+ }
+ r.Body = ConnectIOReader(connection)
+ } else {
+ r.Body = ioutil.NopCloser(&bytes.Buffer{})
+ }
+ defer r.Body.Close()
+
+ if hook, ok := s.impl.(http.Handler); ok {
+ hook.ServeHTTP(w, r)
+ } else {
+ http.NotFound(w, r)
+ }
+
+ return nil
+}
diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go
new file mode 100644
index 000000000..b32a3e36e
--- /dev/null
+++ b/plugin/client_rpc_generated.go
@@ -0,0 +1,1103 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Code generated by "make pluginapi"
+// DO NOT EDIT
+
+package plugin
+
+import (
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func init() {
+ HookNameToId["OnDeactivate"] = OnDeactivateId
+}
+
+type OnDeactivateArgs struct {
+}
+
+type OnDeactivateReturns struct {
+ A error
+}
+
+func (g *HooksRPCClient) OnDeactivate() error {
+ _args := &OnDeactivateArgs{}
+ _returns := &OnDeactivateReturns{}
+ if g.implemented[OnDeactivateId] {
+ if err := g.client.Call("Plugin.OnDeactivate", _args, _returns); err != nil {
+ g.log.Error("RPC call OnDeactivate to plugin failed.", mlog.Err(err))
+ }
+ }
+ return _returns.A
+}
+
+func (s *HooksRPCServer) OnDeactivate(args *OnDeactivateArgs, returns *OnDeactivateReturns) error {
+ if hook, ok := s.impl.(interface {
+ OnDeactivate() error
+ }); ok {
+ returns.A = hook.OnDeactivate()
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["OnConfigurationChange"] = OnConfigurationChangeId
+}
+
+type OnConfigurationChangeArgs struct {
+}
+
+type OnConfigurationChangeReturns struct {
+ A error
+}
+
+func (g *HooksRPCClient) OnConfigurationChange() error {
+ _args := &OnConfigurationChangeArgs{}
+ _returns := &OnConfigurationChangeReturns{}
+ if g.implemented[OnConfigurationChangeId] {
+ if err := g.client.Call("Plugin.OnConfigurationChange", _args, _returns); err != nil {
+ g.log.Error("RPC call OnConfigurationChange to plugin failed.", mlog.Err(err))
+ }
+ }
+ return _returns.A
+}
+
+func (s *HooksRPCServer) OnConfigurationChange(args *OnConfigurationChangeArgs, returns *OnConfigurationChangeReturns) error {
+ if hook, ok := s.impl.(interface {
+ OnConfigurationChange() error
+ }); ok {
+ returns.A = hook.OnConfigurationChange()
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["ExecuteCommand"] = ExecuteCommandId
+}
+
+type ExecuteCommandArgs struct {
+ A *model.CommandArgs
+}
+
+type ExecuteCommandReturns struct {
+ A *model.CommandResponse
+ B *model.AppError
+}
+
+func (g *HooksRPCClient) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
+ _args := &ExecuteCommandArgs{args}
+ _returns := &ExecuteCommandReturns{}
+ if g.implemented[ExecuteCommandId] {
+ if err := g.client.Call("Plugin.ExecuteCommand", _args, _returns); err != nil {
+ g.log.Error("RPC call ExecuteCommand to plugin failed.", mlog.Err(err))
+ }
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *HooksRPCServer) ExecuteCommand(args *ExecuteCommandArgs, returns *ExecuteCommandReturns) error {
+ if hook, ok := s.impl.(interface {
+ ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.ExecuteCommand(args.A)
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["MessageWillBePosted"] = MessageWillBePostedId
+}
+
+type MessageWillBePostedArgs struct {
+ A *model.Post
+}
+
+type MessageWillBePostedReturns struct {
+ A *model.Post
+ B string
+}
+
+func (g *HooksRPCClient) MessageWillBePosted(post *model.Post) (*model.Post, string) {
+ _args := &MessageWillBePostedArgs{post}
+ _returns := &MessageWillBePostedReturns{}
+ if g.implemented[MessageWillBePostedId] {
+ if err := g.client.Call("Plugin.MessageWillBePosted", _args, _returns); err != nil {
+ g.log.Error("RPC call MessageWillBePosted to plugin failed.", mlog.Err(err))
+ }
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *HooksRPCServer) MessageWillBePosted(args *MessageWillBePostedArgs, returns *MessageWillBePostedReturns) error {
+ if hook, ok := s.impl.(interface {
+ MessageWillBePosted(post *model.Post) (*model.Post, string)
+ }); ok {
+ returns.A, returns.B = hook.MessageWillBePosted(args.A)
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["MessageWillBeUpdated"] = MessageWillBeUpdatedId
+}
+
+type MessageWillBeUpdatedArgs struct {
+ A *model.Post
+ B *model.Post
+}
+
+type MessageWillBeUpdatedReturns struct {
+ A *model.Post
+ B string
+}
+
+func (g *HooksRPCClient) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) {
+ _args := &MessageWillBeUpdatedArgs{newPost, oldPost}
+ _returns := &MessageWillBeUpdatedReturns{}
+ if g.implemented[MessageWillBeUpdatedId] {
+ if err := g.client.Call("Plugin.MessageWillBeUpdated", _args, _returns); err != nil {
+ g.log.Error("RPC call MessageWillBeUpdated to plugin failed.", mlog.Err(err))
+ }
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *HooksRPCServer) MessageWillBeUpdated(args *MessageWillBeUpdatedArgs, returns *MessageWillBeUpdatedReturns) error {
+ if hook, ok := s.impl.(interface {
+ MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string)
+ }); ok {
+ returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B)
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["MessageHasBeenPosted"] = MessageHasBeenPostedId
+}
+
+type MessageHasBeenPostedArgs struct {
+ A *model.Post
+}
+
+type MessageHasBeenPostedReturns struct {
+}
+
+func (g *HooksRPCClient) MessageHasBeenPosted(post *model.Post) {
+ _args := &MessageHasBeenPostedArgs{post}
+ _returns := &MessageHasBeenPostedReturns{}
+ if g.implemented[MessageHasBeenPostedId] {
+ if err := g.client.Call("Plugin.MessageHasBeenPosted", _args, _returns); err != nil {
+ g.log.Error("RPC call MessageHasBeenPosted to plugin failed.", mlog.Err(err))
+ }
+ }
+ return
+}
+
+func (s *HooksRPCServer) MessageHasBeenPosted(args *MessageHasBeenPostedArgs, returns *MessageHasBeenPostedReturns) error {
+ if hook, ok := s.impl.(interface {
+ MessageHasBeenPosted(post *model.Post)
+ }); ok {
+ hook.MessageHasBeenPosted(args.A)
+ }
+ return nil
+}
+
+func init() {
+ HookNameToId["MessageHasBeenUpdated"] = MessageHasBeenUpdatedId
+}
+
+type MessageHasBeenUpdatedArgs struct {
+ A *model.Post
+ B *model.Post
+}
+
+type MessageHasBeenUpdatedReturns struct {
+}
+
+func (g *HooksRPCClient) MessageHasBeenUpdated(newPost, oldPost *model.Post) {
+ _args := &MessageHasBeenUpdatedArgs{newPost, oldPost}
+ _returns := &MessageHasBeenUpdatedReturns{}
+ if g.implemented[MessageHasBeenUpdatedId] {
+ if err := g.client.Call("Plugin.MessageHasBeenUpdated", _args, _returns); err != nil {
+ g.log.Error("RPC call MessageHasBeenUpdated to plugin failed.", mlog.Err(err))
+ }
+ }
+ return
+}
+
+func (s *HooksRPCServer) MessageHasBeenUpdated(args *MessageHasBeenUpdatedArgs, returns *MessageHasBeenUpdatedReturns) error {
+ if hook, ok := s.impl.(interface {
+ MessageHasBeenUpdated(newPost, oldPost *model.Post)
+ }); ok {
+ hook.MessageHasBeenUpdated(args.A, args.B)
+ }
+ return nil
+}
+
+type RegisterCommandArgs struct {
+ A *model.Command
+}
+
+type RegisterCommandReturns struct {
+ A error
+}
+
+func (g *APIRPCClient) RegisterCommand(command *model.Command) error {
+ _args := &RegisterCommandArgs{command}
+ _returns := &RegisterCommandReturns{}
+ if err := g.client.Call("Plugin.RegisterCommand", _args, _returns); err != nil {
+ g.log.Error("RPC call to RegisterCommand API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) RegisterCommand(args *RegisterCommandArgs, returns *RegisterCommandReturns) error {
+ if hook, ok := s.impl.(interface {
+ RegisterCommand(command *model.Command) error
+ }); ok {
+ returns.A = hook.RegisterCommand(args.A)
+ }
+ return nil
+}
+
+type UnregisterCommandArgs struct {
+ A string
+ B string
+}
+
+type UnregisterCommandReturns struct {
+ A error
+}
+
+func (g *APIRPCClient) UnregisterCommand(teamId, trigger string) error {
+ _args := &UnregisterCommandArgs{teamId, trigger}
+ _returns := &UnregisterCommandReturns{}
+ if err := g.client.Call("Plugin.UnregisterCommand", _args, _returns); err != nil {
+ g.log.Error("RPC call to UnregisterCommand API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) UnregisterCommand(args *UnregisterCommandArgs, returns *UnregisterCommandReturns) error {
+ if hook, ok := s.impl.(interface {
+ UnregisterCommand(teamId, trigger string) error
+ }); ok {
+ returns.A = hook.UnregisterCommand(args.A, args.B)
+ }
+ return nil
+}
+
+type CreateUserArgs struct {
+ A *model.User
+}
+
+type CreateUserReturns struct {
+ A *model.User
+ B *model.AppError
+}
+
+func (g *APIRPCClient) CreateUser(user *model.User) (*model.User, *model.AppError) {
+ _args := &CreateUserArgs{user}
+ _returns := &CreateUserReturns{}
+ if err := g.client.Call("Plugin.CreateUser", _args, _returns); err != nil {
+ g.log.Error("RPC call to CreateUser API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) CreateUser(args *CreateUserArgs, returns *CreateUserReturns) error {
+ if hook, ok := s.impl.(interface {
+ CreateUser(user *model.User) (*model.User, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.CreateUser(args.A)
+ }
+ return nil
+}
+
+type DeleteUserArgs struct {
+ A string
+}
+
+type DeleteUserReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) DeleteUser(userId string) *model.AppError {
+ _args := &DeleteUserArgs{userId}
+ _returns := &DeleteUserReturns{}
+ if err := g.client.Call("Plugin.DeleteUser", _args, _returns); err != nil {
+ g.log.Error("RPC call to DeleteUser API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) DeleteUser(args *DeleteUserArgs, returns *DeleteUserReturns) error {
+ if hook, ok := s.impl.(interface {
+ DeleteUser(userId string) *model.AppError
+ }); ok {
+ returns.A = hook.DeleteUser(args.A)
+ }
+ return nil
+}
+
+type GetUserArgs struct {
+ A string
+}
+
+type GetUserReturns struct {
+ A *model.User
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetUser(userId string) (*model.User, *model.AppError) {
+ _args := &GetUserArgs{userId}
+ _returns := &GetUserReturns{}
+ if err := g.client.Call("Plugin.GetUser", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetUser API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetUser(args *GetUserArgs, returns *GetUserReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetUser(userId string) (*model.User, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetUser(args.A)
+ }
+ return nil
+}
+
+type GetUserByEmailArgs struct {
+ A string
+}
+
+type GetUserByEmailReturns struct {
+ A *model.User
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetUserByEmail(email string) (*model.User, *model.AppError) {
+ _args := &GetUserByEmailArgs{email}
+ _returns := &GetUserByEmailReturns{}
+ if err := g.client.Call("Plugin.GetUserByEmail", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetUserByEmail API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetUserByEmail(args *GetUserByEmailArgs, returns *GetUserByEmailReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetUserByEmail(email string) (*model.User, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetUserByEmail(args.A)
+ }
+ return nil
+}
+
+type GetUserByUsernameArgs struct {
+ A string
+}
+
+type GetUserByUsernameReturns struct {
+ A *model.User
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetUserByUsername(name string) (*model.User, *model.AppError) {
+ _args := &GetUserByUsernameArgs{name}
+ _returns := &GetUserByUsernameReturns{}
+ if err := g.client.Call("Plugin.GetUserByUsername", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetUserByUsername API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetUserByUsername(args *GetUserByUsernameArgs, returns *GetUserByUsernameReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetUserByUsername(name string) (*model.User, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetUserByUsername(args.A)
+ }
+ return nil
+}
+
+type UpdateUserArgs struct {
+ A *model.User
+}
+
+type UpdateUserReturns struct {
+ A *model.User
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdateUser(user *model.User) (*model.User, *model.AppError) {
+ _args := &UpdateUserArgs{user}
+ _returns := &UpdateUserReturns{}
+ if err := g.client.Call("Plugin.UpdateUser", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdateUser API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdateUser(args *UpdateUserArgs, returns *UpdateUserReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdateUser(user *model.User) (*model.User, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdateUser(args.A)
+ }
+ return nil
+}
+
+type CreateTeamArgs struct {
+ A *model.Team
+}
+
+type CreateTeamReturns struct {
+ A *model.Team
+ B *model.AppError
+}
+
+func (g *APIRPCClient) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
+ _args := &CreateTeamArgs{team}
+ _returns := &CreateTeamReturns{}
+ if err := g.client.Call("Plugin.CreateTeam", _args, _returns); err != nil {
+ g.log.Error("RPC call to CreateTeam API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) CreateTeam(args *CreateTeamArgs, returns *CreateTeamReturns) error {
+ if hook, ok := s.impl.(interface {
+ CreateTeam(team *model.Team) (*model.Team, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.CreateTeam(args.A)
+ }
+ return nil
+}
+
+type DeleteTeamArgs struct {
+ A string
+}
+
+type DeleteTeamReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) DeleteTeam(teamId string) *model.AppError {
+ _args := &DeleteTeamArgs{teamId}
+ _returns := &DeleteTeamReturns{}
+ if err := g.client.Call("Plugin.DeleteTeam", _args, _returns); err != nil {
+ g.log.Error("RPC call to DeleteTeam API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) DeleteTeam(args *DeleteTeamArgs, returns *DeleteTeamReturns) error {
+ if hook, ok := s.impl.(interface {
+ DeleteTeam(teamId string) *model.AppError
+ }); ok {
+ returns.A = hook.DeleteTeam(args.A)
+ }
+ return nil
+}
+
+type GetTeamArgs struct {
+ A string
+}
+
+type GetTeamReturns struct {
+ A *model.Team
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetTeam(teamId string) (*model.Team, *model.AppError) {
+ _args := &GetTeamArgs{teamId}
+ _returns := &GetTeamReturns{}
+ if err := g.client.Call("Plugin.GetTeam", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetTeam API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetTeam(args *GetTeamArgs, returns *GetTeamReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetTeam(teamId string) (*model.Team, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetTeam(args.A)
+ }
+ return nil
+}
+
+type GetTeamByNameArgs struct {
+ A string
+}
+
+type GetTeamByNameReturns struct {
+ A *model.Team
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetTeamByName(name string) (*model.Team, *model.AppError) {
+ _args := &GetTeamByNameArgs{name}
+ _returns := &GetTeamByNameReturns{}
+ if err := g.client.Call("Plugin.GetTeamByName", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetTeamByName API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetTeamByName(args *GetTeamByNameArgs, returns *GetTeamByNameReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetTeamByName(name string) (*model.Team, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetTeamByName(args.A)
+ }
+ return nil
+}
+
+type UpdateTeamArgs struct {
+ A *model.Team
+}
+
+type UpdateTeamReturns struct {
+ A *model.Team
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
+ _args := &UpdateTeamArgs{team}
+ _returns := &UpdateTeamReturns{}
+ if err := g.client.Call("Plugin.UpdateTeam", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdateTeam API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdateTeam(args *UpdateTeamArgs, returns *UpdateTeamReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdateTeam(team *model.Team) (*model.Team, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdateTeam(args.A)
+ }
+ return nil
+}
+
+type CreateChannelArgs struct {
+ A *model.Channel
+}
+
+type CreateChannelReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+ _args := &CreateChannelArgs{channel}
+ _returns := &CreateChannelReturns{}
+ if err := g.client.Call("Plugin.CreateChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to CreateChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) CreateChannel(args *CreateChannelArgs, returns *CreateChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.CreateChannel(args.A)
+ }
+ return nil
+}
+
+type DeleteChannelArgs struct {
+ A string
+}
+
+type DeleteChannelReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) DeleteChannel(channelId string) *model.AppError {
+ _args := &DeleteChannelArgs{channelId}
+ _returns := &DeleteChannelReturns{}
+ if err := g.client.Call("Plugin.DeleteChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to DeleteChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) DeleteChannel(args *DeleteChannelArgs, returns *DeleteChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ DeleteChannel(channelId string) *model.AppError
+ }); ok {
+ returns.A = hook.DeleteChannel(args.A)
+ }
+ return nil
+}
+
+type GetChannelArgs struct {
+ A string
+}
+
+type GetChannelReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetChannel(channelId string) (*model.Channel, *model.AppError) {
+ _args := &GetChannelArgs{channelId}
+ _returns := &GetChannelReturns{}
+ if err := g.client.Call("Plugin.GetChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetChannel(args *GetChannelArgs, returns *GetChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetChannel(channelId string) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetChannel(args.A)
+ }
+ return nil
+}
+
+type GetChannelByNameArgs struct {
+ A string
+ B string
+}
+
+type GetChannelByNameReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
+ _args := &GetChannelByNameArgs{name, teamId}
+ _returns := &GetChannelByNameReturns{}
+ if err := g.client.Call("Plugin.GetChannelByName", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetChannelByName API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetChannelByName(args *GetChannelByNameArgs, returns *GetChannelByNameReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetChannelByName(name, teamId string) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetChannelByName(args.A, args.B)
+ }
+ return nil
+}
+
+type GetDirectChannelArgs struct {
+ A string
+ B string
+}
+
+type GetDirectChannelReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
+ _args := &GetDirectChannelArgs{userId1, userId2}
+ _returns := &GetDirectChannelReturns{}
+ if err := g.client.Call("Plugin.GetDirectChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetDirectChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetDirectChannel(args *GetDirectChannelArgs, returns *GetDirectChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetDirectChannel(args.A, args.B)
+ }
+ return nil
+}
+
+type GetGroupChannelArgs struct {
+ A []string
+}
+
+type GetGroupChannelReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
+ _args := &GetGroupChannelArgs{userIds}
+ _returns := &GetGroupChannelReturns{}
+ if err := g.client.Call("Plugin.GetGroupChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetGroupChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetGroupChannel(args *GetGroupChannelArgs, returns *GetGroupChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetGroupChannel(userIds []string) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetGroupChannel(args.A)
+ }
+ return nil
+}
+
+type UpdateChannelArgs struct {
+ A *model.Channel
+}
+
+type UpdateChannelReturns struct {
+ A *model.Channel
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+ _args := &UpdateChannelArgs{channel}
+ _returns := &UpdateChannelReturns{}
+ if err := g.client.Call("Plugin.UpdateChannel", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdateChannel API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdateChannel(args *UpdateChannelArgs, returns *UpdateChannelReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdateChannel(args.A)
+ }
+ return nil
+}
+
+type AddChannelMemberArgs struct {
+ A string
+ B string
+}
+
+type AddChannelMemberReturns struct {
+ A *model.ChannelMember
+ B *model.AppError
+}
+
+func (g *APIRPCClient) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ _args := &AddChannelMemberArgs{channelId, userId}
+ _returns := &AddChannelMemberReturns{}
+ if err := g.client.Call("Plugin.AddChannelMember", _args, _returns); err != nil {
+ g.log.Error("RPC call to AddChannelMember API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) AddChannelMember(args *AddChannelMemberArgs, returns *AddChannelMemberReturns) error {
+ if hook, ok := s.impl.(interface {
+ AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.AddChannelMember(args.A, args.B)
+ }
+ return nil
+}
+
+type GetChannelMemberArgs struct {
+ A string
+ B string
+}
+
+type GetChannelMemberReturns struct {
+ A *model.ChannelMember
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ _args := &GetChannelMemberArgs{channelId, userId}
+ _returns := &GetChannelMemberReturns{}
+ if err := g.client.Call("Plugin.GetChannelMember", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetChannelMember API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetChannelMember(args *GetChannelMemberArgs, returns *GetChannelMemberReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetChannelMember(args.A, args.B)
+ }
+ return nil
+}
+
+type UpdateChannelMemberRolesArgs struct {
+ A string
+ B string
+ C string
+}
+
+type UpdateChannelMemberRolesReturns struct {
+ A *model.ChannelMember
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
+ _args := &UpdateChannelMemberRolesArgs{channelId, userId, newRoles}
+ _returns := &UpdateChannelMemberRolesReturns{}
+ if err := g.client.Call("Plugin.UpdateChannelMemberRoles", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdateChannelMemberRoles API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdateChannelMemberRoles(args *UpdateChannelMemberRolesArgs, returns *UpdateChannelMemberRolesReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdateChannelMemberRoles(args.A, args.B, args.C)
+ }
+ return nil
+}
+
+type UpdateChannelMemberNotificationsArgs struct {
+ A string
+ B string
+ C map[string]string
+}
+
+type UpdateChannelMemberNotificationsReturns struct {
+ A *model.ChannelMember
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+ _args := &UpdateChannelMemberNotificationsArgs{channelId, userId, notifications}
+ _returns := &UpdateChannelMemberNotificationsReturns{}
+ if err := g.client.Call("Plugin.UpdateChannelMemberNotifications", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdateChannelMemberNotifications API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdateChannelMemberNotifications(args *UpdateChannelMemberNotificationsArgs, returns *UpdateChannelMemberNotificationsReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdateChannelMemberNotifications(args.A, args.B, args.C)
+ }
+ return nil
+}
+
+type DeleteChannelMemberArgs struct {
+ A string
+ B string
+}
+
+type DeleteChannelMemberReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) DeleteChannelMember(channelId, userId string) *model.AppError {
+ _args := &DeleteChannelMemberArgs{channelId, userId}
+ _returns := &DeleteChannelMemberReturns{}
+ if err := g.client.Call("Plugin.DeleteChannelMember", _args, _returns); err != nil {
+ g.log.Error("RPC call to DeleteChannelMember API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) DeleteChannelMember(args *DeleteChannelMemberArgs, returns *DeleteChannelMemberReturns) error {
+ if hook, ok := s.impl.(interface {
+ DeleteChannelMember(channelId, userId string) *model.AppError
+ }); ok {
+ returns.A = hook.DeleteChannelMember(args.A, args.B)
+ }
+ return nil
+}
+
+type CreatePostArgs struct {
+ A *model.Post
+}
+
+type CreatePostReturns struct {
+ A *model.Post
+ B *model.AppError
+}
+
+func (g *APIRPCClient) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
+ _args := &CreatePostArgs{post}
+ _returns := &CreatePostReturns{}
+ if err := g.client.Call("Plugin.CreatePost", _args, _returns); err != nil {
+ g.log.Error("RPC call to CreatePost API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) CreatePost(args *CreatePostArgs, returns *CreatePostReturns) error {
+ if hook, ok := s.impl.(interface {
+ CreatePost(post *model.Post) (*model.Post, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.CreatePost(args.A)
+ }
+ return nil
+}
+
+type DeletePostArgs struct {
+ A string
+}
+
+type DeletePostReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) DeletePost(postId string) *model.AppError {
+ _args := &DeletePostArgs{postId}
+ _returns := &DeletePostReturns{}
+ if err := g.client.Call("Plugin.DeletePost", _args, _returns); err != nil {
+ g.log.Error("RPC call to DeletePost API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) DeletePost(args *DeletePostArgs, returns *DeletePostReturns) error {
+ if hook, ok := s.impl.(interface {
+ DeletePost(postId string) *model.AppError
+ }); ok {
+ returns.A = hook.DeletePost(args.A)
+ }
+ return nil
+}
+
+type GetPostArgs struct {
+ A string
+}
+
+type GetPostReturns struct {
+ A *model.Post
+ B *model.AppError
+}
+
+func (g *APIRPCClient) GetPost(postId string) (*model.Post, *model.AppError) {
+ _args := &GetPostArgs{postId}
+ _returns := &GetPostReturns{}
+ if err := g.client.Call("Plugin.GetPost", _args, _returns); err != nil {
+ g.log.Error("RPC call to GetPost API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) GetPost(args *GetPostArgs, returns *GetPostReturns) error {
+ if hook, ok := s.impl.(interface {
+ GetPost(postId string) (*model.Post, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.GetPost(args.A)
+ }
+ return nil
+}
+
+type UpdatePostArgs struct {
+ A *model.Post
+}
+
+type UpdatePostReturns struct {
+ A *model.Post
+ B *model.AppError
+}
+
+func (g *APIRPCClient) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
+ _args := &UpdatePostArgs{post}
+ _returns := &UpdatePostReturns{}
+ if err := g.client.Call("Plugin.UpdatePost", _args, _returns); err != nil {
+ g.log.Error("RPC call to UpdatePost API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) UpdatePost(args *UpdatePostArgs, returns *UpdatePostReturns) error {
+ if hook, ok := s.impl.(interface {
+ UpdatePost(post *model.Post) (*model.Post, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.UpdatePost(args.A)
+ }
+ return nil
+}
+
+type KVSetArgs struct {
+ A string
+ B []byte
+}
+
+type KVSetReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) KVSet(key string, value []byte) *model.AppError {
+ _args := &KVSetArgs{key, value}
+ _returns := &KVSetReturns{}
+ if err := g.client.Call("Plugin.KVSet", _args, _returns); err != nil {
+ g.log.Error("RPC call to KVSet API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) KVSet(args *KVSetArgs, returns *KVSetReturns) error {
+ if hook, ok := s.impl.(interface {
+ KVSet(key string, value []byte) *model.AppError
+ }); ok {
+ returns.A = hook.KVSet(args.A, args.B)
+ }
+ return nil
+}
+
+type KVGetArgs struct {
+ A string
+}
+
+type KVGetReturns struct {
+ A []byte
+ B *model.AppError
+}
+
+func (g *APIRPCClient) KVGet(key string) ([]byte, *model.AppError) {
+ _args := &KVGetArgs{key}
+ _returns := &KVGetReturns{}
+ if err := g.client.Call("Plugin.KVGet", _args, _returns); err != nil {
+ g.log.Error("RPC call to KVGet API failed.", mlog.Err(err))
+ }
+ return _returns.A, _returns.B
+}
+
+func (s *APIRPCServer) KVGet(args *KVGetArgs, returns *KVGetReturns) error {
+ if hook, ok := s.impl.(interface {
+ KVGet(key string) ([]byte, *model.AppError)
+ }); ok {
+ returns.A, returns.B = hook.KVGet(args.A)
+ }
+ return nil
+}
+
+type KVDeleteArgs struct {
+ A string
+}
+
+type KVDeleteReturns struct {
+ A *model.AppError
+}
+
+func (g *APIRPCClient) KVDelete(key string) *model.AppError {
+ _args := &KVDeleteArgs{key}
+ _returns := &KVDeleteReturns{}
+ if err := g.client.Call("Plugin.KVDelete", _args, _returns); err != nil {
+ g.log.Error("RPC call to KVDelete API failed.", mlog.Err(err))
+ }
+ return _returns.A
+}
+
+func (s *APIRPCServer) KVDelete(args *KVDeleteArgs, returns *KVDeleteReturns) error {
+ if hook, ok := s.impl.(interface {
+ KVDelete(key string) *model.AppError
+ }); ok {
+ returns.A = hook.KVDelete(args.A)
+ }
+ return nil
+}
diff --git a/plugin/environment.go b/plugin/environment.go
new file mode 100644
index 000000000..de67225d2
--- /dev/null
+++ b/plugin/environment.go
@@ -0,0 +1,260 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package plugin
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "sync"
+
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/pkg/errors"
+)
+
+type APIImplCreatorFunc func(*model.Manifest) API
+type SupervisorCreatorFunc func(*model.BundleInfo, *mlog.Logger, API) (*Supervisor, error)
+
+// Hooks will be the hooks API for the plugin
+// Return value should be true if we should continue calling more plugins
+type MultliPluginHookRunnerFunc func(hooks Hooks) bool
+
+type ActivePlugin struct {
+ BundleInfo *model.BundleInfo
+ State int
+ Supervisor *Supervisor
+}
+
+type Environment struct {
+ activePlugins map[string]ActivePlugin
+ mutex sync.RWMutex
+ logger *mlog.Logger
+ newAPIImpl APIImplCreatorFunc
+ pluginDir string
+ webappPluginDir string
+}
+
+func NewEnvironment(newAPIImpl APIImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) {
+ return &Environment{
+ activePlugins: make(map[string]ActivePlugin),
+ 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 {
+ env.mutex.RLock()
+ defer env.mutex.RUnlock()
+
+ activePlugins := []*model.BundleInfo{}
+ for _, p := range env.activePlugins {
+ activePlugins = append(activePlugins, p.BundleInfo)
+ }
+
+ return activePlugins
+}
+
+func (env *Environment) IsActive(id string) bool {
+ _, ok := env.activePlugins[id]
+ return ok
+}
+
+// Returns a list of plugin statuses reprensenting the state of every plugin
+func (env *Environment) Statuses() (model.PluginStatuses, error) {
+ env.mutex.RLock()
+ defer env.mutex.RUnlock()
+
+ 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[plugin.Manifest.Id]; ok {
+ pluginState = plugin.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) (reterr error) {
+ env.mutex.Lock()
+ defer env.mutex.Unlock()
+
+ // Check if we are already active
+ if _, ok := env.activePlugins[id]; ok {
+ return nil
+ }
+
+ plugins, err := env.Available()
+ if err != nil {
+ return err
+ }
+ var pluginInfo *model.BundleInfo
+ for _, p := range plugins {
+ if p.Manifest != nil && p.Manifest.Id == id {
+ if pluginInfo != nil {
+ return fmt.Errorf("multiple plugins found: %v", id)
+ }
+ pluginInfo = p
+ }
+ }
+ if pluginInfo == nil {
+ return 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[pluginInfo.Manifest.Id] = activePlugin
+ }()
+
+ if pluginInfo.Manifest.Webapp != nil {
+ bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath)
+ if bundlePath == "" || bundlePath[0] == '.' {
+ return fmt.Errorf("invalid webapp bundle path")
+ }
+ bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
+
+ webappBundle, err := ioutil.ReadFile(bundlePath)
+ if err != nil {
+ return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
+ }
+
+ err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPluginDir, id), webappBundle, 0644)
+ if err != nil {
+ return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
+ }
+ }
+
+ if pluginInfo.Manifest.Backend != nil {
+ supervisor, err := NewSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest))
+ if err != nil {
+ return errors.Wrapf(err, "unable to start plugin: %v", id)
+ }
+ activePlugin.Supervisor = supervisor
+ }
+
+ return nil
+}
+
+// Deactivates the plugin with the given id.
+func (env *Environment) Deactivate(id string) {
+ env.mutex.Lock()
+ defer env.mutex.Unlock()
+
+ if activePlugin, ok := env.activePlugins[id]; !ok {
+ return
+ } else {
+ delete(env.activePlugins, id)
+ 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()
+ }
+ }
+}
+
+// Deactivates all plugins and gracefully shuts down the environment.
+func (env *Environment) Shutdown() {
+ env.mutex.Lock()
+ defer env.mutex.Unlock()
+
+ for _, activePlugin := range env.activePlugins {
+ 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 = make(map[string]ActivePlugin)
+ return
+}
+
+// Returns the hooks API for the plugin ID specified
+// You should probably use RunMultiPluginHook instead.
+func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
+ env.mutex.RLock()
+ defer env.mutex.RUnlock()
+
+ if plug, ok := env.activePlugins[id]; ok && plug.Supervisor != nil {
+ return plug.Supervisor.Hooks(), nil
+ }
+
+ return nil, fmt.Errorf("plugin not found: %v", id)
+}
+
+// Calls hookRunnerFunc with the hooks for each active plugin that implments the given HookId
+// If hookRunnerFunc returns false, then iteration will not continue.
+func (env *Environment) RunMultiPluginHook(hookRunnerFunc MultliPluginHookRunnerFunc, mustImplement int) {
+ env.mutex.RLock()
+ defer env.mutex.RUnlock()
+
+ for _, activePlugin := range env.activePlugins {
+ if activePlugin.Supervisor == nil || !activePlugin.Supervisor.Implements(mustImplement) {
+ continue
+ }
+ if !hookRunnerFunc(activePlugin.Supervisor.Hooks()) {
+ break
+ }
+ }
+}
diff --git a/plugin/example_hello_user_test.go b/plugin/example_hello_user_test.go
deleted file mode 100644
index 989cca0f2..000000000
--- a/plugin/example_hello_user_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package plugin_test
-
-import (
- "fmt"
- "net/http"
-
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-type HelloUserPlugin struct {
- api plugin.API
-}
-
-func (p *HelloUserPlugin) OnActivate(api plugin.API) error {
- // Just save api for later when we need to look up users.
- p.api = api
- return nil
-}
-
-func (p *HelloUserPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if userId := r.Header.Get("Mattermost-User-Id"); userId == "" {
- // Our visitor is unauthenticated.
- fmt.Fprintf(w, "Hello, stranger!")
- } else if user, err := p.api.GetUser(userId); err == nil {
- // Greet the user by name!
- fmt.Fprintf(w, "Welcome back, %v!", user.Username)
- } else {
- // This won't happen in normal circumstances, but let's just be safe.
- w.WriteHeader(http.StatusInternalServerError)
- fmt.Fprintf(w, err.Error())
- }
-}
-
-// This example demonstrates a plugin that handles HTTP requests which respond by greeting the user
-// by name.
-func Example_helloUser() {
- rpcplugin.Main(&HelloUserPlugin{})
-}
diff --git a/plugin/example_hello_world_test.go b/plugin/example_hello_world_test.go
deleted file mode 100644
index 5dea28823..000000000
--- a/plugin/example_hello_world_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package plugin_test
-
-import (
- "fmt"
- "net/http"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-type HelloWorldPlugin struct{}
-
-func (p *HelloWorldPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Hello, world!")
-}
-
-// This example demonstrates a plugin that handles HTTP requests which respond by greeting the
-// world.
-func Example_helloWorld() {
- rpcplugin.Main(&HelloWorldPlugin{})
-}
diff --git a/plugin/example_test.go b/plugin/example_test.go
deleted file mode 100644
index e6ae3c2ea..000000000
--- a/plugin/example_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package plugin_test
-
-import (
- "io/ioutil"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
-)
-
-func TestHelloUserPlugin(t *testing.T) {
- user := &model.User{
- Id: model.NewId(),
- Username: "billybob",
- }
-
- api := &plugintest.API{}
- api.On("GetUser", user.Id).Return(user, nil)
- defer api.AssertExpectations(t)
-
- p := &HelloUserPlugin{}
- p.OnActivate(api)
-
- w := httptest.NewRecorder()
- r := httptest.NewRequest("GET", "/", nil)
- r.Header.Add("Mattermost-User-Id", user.Id)
- p.ServeHTTP(w, r)
- body, err := ioutil.ReadAll(w.Result().Body)
- require.NoError(t, err)
- assert.Equal(t, "Welcome back, billybob!", string(body))
-}
diff --git a/plugin/hclog_adapter.go b/plugin/hclog_adapter.go
new file mode 100644
index 000000000..c8e39877e
--- /dev/null
+++ b/plugin/hclog_adapter.go
@@ -0,0 +1,73 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package plugin
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/hashicorp/go-hclog"
+ "github.com/mattermost/mattermost-server/mlog"
+)
+
+type HclogAdapter struct {
+ wrappedLogger *mlog.Logger
+ extrasKey string
+}
+
+func (h *HclogAdapter) Trace(msg string, args ...interface{}) {
+ h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
+}
+
+func (h *HclogAdapter) Debug(msg string, args ...interface{}) {
+ h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
+}
+
+func (h *HclogAdapter) Info(msg string, args ...interface{}) {
+ h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
+}
+
+func (h *HclogAdapter) Warn(msg string, args ...interface{}) {
+ h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
+}
+
+func (h *HclogAdapter) Error(msg string, args ...interface{}) {
+ h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
+}
+
+func (h *HclogAdapter) IsTrace() bool {
+ return false
+}
+
+func (h *HclogAdapter) IsDebug() bool {
+ return true
+}
+
+func (h *HclogAdapter) IsInfo() bool {
+ return true
+}
+
+func (h *HclogAdapter) IsWarn() bool {
+ return true
+}
+
+func (h *HclogAdapter) IsError() bool {
+ return true
+}
+
+func (h *HclogAdapter) With(args ...interface{}) hclog.Logger {
+ return h
+}
+
+func (h *HclogAdapter) Named(name string) hclog.Logger {
+ return h
+}
+
+func (h *HclogAdapter) ResetNamed(name string) hclog.Logger {
+ return h
+}
+
+func (h *HclogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
+ return h.wrappedLogger.StdLog()
+}
diff --git a/plugin/hooks.go b/plugin/hooks.go
index e41081e48..68eca8ede 100644
--- a/plugin/hooks.go
+++ b/plugin/hooks.go
@@ -9,15 +9,32 @@ import (
"github.com/mattermost/mattermost-server/model"
)
+// These assignments are part of the wire protocol. You can add more, but should not change existing
+// assignments. Follow the naming convention of <HookName>Id as the autogenerated glue code depends on that.
+const (
+ OnActivateId = 0
+ OnDeactivateId = 1
+ ServeHTTPId = 2
+ OnConfigurationChangeId = 3
+ ExecuteCommandId = 4
+ MessageWillBePostedId = 5
+ MessageWillBeUpdatedId = 6
+ MessageHasBeenPostedId = 7
+ MessageHasBeenUpdatedId = 8
+ TotalHooksId = iota
+)
+
// Methods from the Hooks interface can be used by a plugin to respond to events. Methods are likely
// to be added over time, and plugins are not expected to implement all of them. Instead, plugins
// are expected to implement a subset of them and pass an instance to plugin/rpcplugin.Main, which
// will take over execution of the process and add default behaviors for missing hooks.
type Hooks interface {
- // OnActivate is invoked when the plugin is activated. Implementations will usually want to save
- // the api argument for later use. Loading configuration for the first time is also a commonly
- // done here.
- OnActivate(API) error
+ // OnActivate is invoked when the plugin is activated.
+ OnActivate() error
+
+ // Implemented returns a list of hooks that are implmented by the plugin.
+ // Plugins do not need to provide an implementation. Any given will be ignored.
+ Implemented() ([]string, error)
// OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to
// use the API, and the plugin will be terminated shortly after this invocation.
@@ -31,7 +48,7 @@ type Hooks interface {
//
// The Mattermost-User-Id header will be present if (and only if) the request is by an
// authenticated user.
- ServeHTTP(http.ResponseWriter, *http.Request)
+ ServeHTTP(w http.ResponseWriter, r *http.Request)
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand
// API.
diff --git a/plugin/http.go b/plugin/http.go
new file mode 100644
index 000000000..5faf8f08a
--- /dev/null
+++ b/plugin/http.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package plugin
+
+import (
+ "io"
+ "net/http"
+ "net/rpc"
+)
+
+type HTTPResponseWriterRPCServer struct {
+ w http.ResponseWriter
+}
+
+func (w *HTTPResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error {
+ *reply = w.w.Header()
+ return nil
+}
+
+func (w *HTTPResponseWriterRPCServer) Write(args []byte, reply *struct{}) error {
+ _, err := w.w.Write(args)
+ return err
+}
+
+func (w *HTTPResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error {
+ w.w.WriteHeader(args)
+ return nil
+}
+
+func (w *HTTPResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error {
+ dest := w.w.Header()
+ for k := range dest {
+ if _, ok := args[k]; !ok {
+ delete(dest, k)
+ }
+ }
+ for k, v := range args {
+ dest[k] = v
+ }
+ return nil
+}
+
+func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) {
+ server := rpc.NewServer()
+ server.Register(&HTTPResponseWriterRPCServer{
+ w: w,
+ })
+ server.ServeConn(conn)
+}
+
+type HTTPResponseWriterRPCClient struct {
+ client *rpc.Client
+ header http.Header
+}
+
+var _ http.ResponseWriter = (*HTTPResponseWriterRPCClient)(nil)
+
+func (w *HTTPResponseWriterRPCClient) Header() http.Header {
+ if w.header == nil {
+ w.client.Call("Plugin.Header", struct{}{}, &w.header)
+ }
+ return w.header
+}
+
+func (w *HTTPResponseWriterRPCClient) Write(b []byte) (int, error) {
+ if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
+ return 0, err
+ }
+ if err := w.client.Call("Plugin.Write", b, nil); err != nil {
+ return 0, err
+ }
+ return len(b), nil
+}
+
+func (w *HTTPResponseWriterRPCClient) WriteHeader(statusCode int) {
+ if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
+ return
+ }
+ w.client.Call("Plugin.WriteHeader", statusCode, nil)
+}
+
+func (h *HTTPResponseWriterRPCClient) Close() error {
+ return h.client.Close()
+}
+
+func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *HTTPResponseWriterRPCClient {
+ return &HTTPResponseWriterRPCClient{
+ client: rpc.NewClient(conn),
+ }
+}
diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go
new file mode 100644
index 000000000..5f66506d3
--- /dev/null
+++ b/plugin/interface_generator/main.go
@@ -0,0 +1,377 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/printer"
+ "go/token"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/alecthomas/template"
+ "github.com/pkg/errors"
+)
+
+type IHookEntry struct {
+ FuncName string
+ Args *ast.FieldList
+ Results *ast.FieldList
+}
+
+type PluginInterfaceInfo struct {
+ Hooks []IHookEntry
+ API []IHookEntry
+ FileSet *token.FileSet
+}
+
+func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string {
+ result := []string{}
+ if fieldList == nil || len(fieldList.List) == 0 {
+ return "()"
+ }
+ for _, field := range fieldList.List {
+ typeNameBuffer := &bytes.Buffer{}
+ err := printer.Fprint(typeNameBuffer, fileset, field.Type)
+ if err != nil {
+ panic(err)
+ }
+ typeName := typeNameBuffer.String()
+ names := []string{}
+ for _, name := range field.Names {
+ names = append(names, name.Name)
+ }
+ result = append(result, strings.Join(names, ", ")+" "+typeName)
+ }
+
+ return "(" + strings.Join(result, ", ") + ")"
+}
+
+func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet) string {
+ result := []string{}
+ if fieldList == nil || len(fieldList.List) == 0 {
+ return ""
+ }
+ for _, field := range fieldList.List {
+ for _, name := range field.Names {
+ result = append(result, name.Name)
+ }
+ }
+
+ return strings.Join(result, ", ")
+}
+
+func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
+ result := []string{}
+ if fieldList == nil || len(fieldList.List) == 0 {
+ return ""
+ }
+ nextLetter := 'A'
+ for _, field := range fieldList.List {
+ if len(field.Names) == 0 {
+ result = append(result, structPrefix+string(nextLetter))
+ nextLetter += 1
+ } else {
+ for range field.Names {
+ result = append(result, structPrefix+string(nextLetter))
+ nextLetter += 1
+ }
+ }
+ }
+
+ return strings.Join(result, ", ")
+}
+
+func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string {
+ result := []string{}
+ if fieldList == nil || len(fieldList.List) == 0 {
+ return ""
+ }
+ nextLetter := 'A'
+ for _, field := range fieldList.List {
+ typeNameBuffer := &bytes.Buffer{}
+ err := printer.Fprint(typeNameBuffer, fileset, field.Type)
+ if err != nil {
+ panic(err)
+ }
+ typeName := typeNameBuffer.String()
+ if len(field.Names) == 0 {
+ result = append(result, string(nextLetter)+" "+typeName)
+ nextLetter += 1
+ } else {
+ for range field.Names {
+ result = append(result, string(nextLetter)+" "+typeName)
+ nextLetter += 1
+ }
+ }
+ }
+
+ return strings.Join(result, "\n\t")
+}
+
+func goList(dir string) ([]string, error) {
+ cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir)
+ bytes, err := cmd.Output()
+ if err != nil {
+ return nil, errors.Wrap(err, "Can't list packages")
+ }
+
+ return strings.Fields(string(bytes)), nil
+}
+
+func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) {
+ info.Hooks = append(info.Hooks, IHookEntry{
+ FuncName: method.Names[0].Name,
+ Args: method.Type.(*ast.FuncType).Params,
+ Results: method.Type.(*ast.FuncType).Results,
+ })
+}
+
+func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) {
+ info.API = append(info.API, IHookEntry{
+ FuncName: method.Names[0].Name,
+ Args: method.Type.(*ast.FuncType).Params,
+ Results: method.Type.(*ast.FuncType).Results,
+ })
+}
+
+func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool {
+ return func(node ast.Node) bool {
+ if typeSpec, ok := node.(*ast.TypeSpec); ok {
+ if typeSpec.Name.Name == "Hooks" {
+ for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
+ info.addHookMethod(method)
+ }
+ return false
+ } else if typeSpec.Name.Name == "API" {
+ for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
+ info.addAPIMethod(method)
+ }
+ return false
+ }
+ }
+ return true
+ }
+}
+
+func getPluginInfo(dir string) (*PluginInterfaceInfo, error) {
+ pluginInfo := &PluginInterfaceInfo{
+ Hooks: make([]IHookEntry, 0),
+ FileSet: token.NewFileSet(),
+ }
+
+ packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments)
+ if err != nil {
+ log.Println("Parser error in dir "+dir+": ", err)
+ }
+
+ for _, pkg := range packages {
+ if pkg.Name != "plugin" {
+ continue
+ }
+
+ for _, file := range pkg.Files {
+ ast.Inspect(file, pluginInfo.makeHookInspector())
+ }
+ }
+
+ return pluginInfo, nil
+}
+
+var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+// Code generated by "make pluginapi"
+// DO NOT EDIT
+
+package plugin
+
+{{range .HooksMethods}}
+
+func init() {
+ HookNameToId["{{.Name}}"] = {{.Name}}Id
+}
+
+type {{.Name}}Args struct {
+ {{structStyle .Params}}
+}
+
+type {{.Name}}Returns struct {
+ {{structStyle .Return}}
+}
+
+func (g *HooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
+ _args := &{{.Name}}Args{ {{valuesOnly .Params}} }
+ _returns := &{{.Name}}Returns{}
+ if g.implemented[{{.Name}}Id] {
+ if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
+ g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err))
+ }
+ }
+ return {{destruct "_returns." .Return}}
+}
+
+func (s *HooksRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error {
+ if hook, ok := s.impl.(interface {
+ {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
+ }); ok {
+ {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
+ }
+ return nil
+}
+{{end}}
+
+{{range .APIMethods}}
+
+type {{.Name}}Args struct {
+ {{structStyle .Params}}
+}
+
+type {{.Name}}Returns struct {
+ {{structStyle .Return}}
+}
+
+func (g *APIRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
+ _args := &{{.Name}}Args{ {{valuesOnly .Params}} }
+ _returns := &{{.Name}}Returns{}
+ if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
+ g.log.Error("RPC call to {{.Name}} API failed.", mlog.Err(err))
+ }
+ return {{destruct "_returns." .Return}}
+}
+
+func (s *APIRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error {
+ if hook, ok := s.impl.(interface {
+ {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
+ }); ok {
+ {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
+ }
+ return nil
+}
+{{end}}
+`
+
+type MethodParams struct {
+ Name string
+ Params *ast.FieldList
+ Return *ast.FieldList
+}
+
+type HooksTemplateParams struct {
+ HooksMethods []MethodParams
+ APIMethods []MethodParams
+}
+
+func generateGlue(info *PluginInterfaceInfo) {
+ templateFunctions := map[string]interface{}{
+ "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
+ "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
+ "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) },
+ "destruct": func(structPrefix string, fields *ast.FieldList) string {
+ return FieldListDestruct(structPrefix, fields, info.FileSet)
+ },
+ }
+
+ hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate)
+ if err != nil {
+ panic(err)
+ }
+
+ templateParams := HooksTemplateParams{}
+ for _, hook := range info.Hooks {
+ templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
+ Name: hook.FuncName,
+ Params: hook.Args,
+ Return: hook.Results,
+ })
+ }
+ for _, api := range info.API {
+ templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
+ Name: api.FuncName,
+ Params: api.Args,
+ Return: api.Results,
+ })
+ }
+ templateResult := &bytes.Buffer{}
+ hooksTemplate.Execute(templateResult, &templateParams)
+
+ importsBuffer := &bytes.Buffer{}
+ cmd := exec.Command("goimports")
+ cmd.Stdin = templateResult
+ cmd.Stdout = importsBuffer
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ panic(err)
+ }
+
+ if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), importsBuffer.Bytes(), 0664); err != nil {
+ panic(err)
+ }
+}
+
+func getPluginPackageDir() string {
+ dirs, err := goList("github.com/mattermost/mattermost-server/plugin")
+ if err != nil {
+ panic(err)
+ } else if len(dirs) != 1 {
+ panic("More than one package dir, or no dirs!")
+ }
+
+ return dirs[0]
+}
+
+func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo {
+ toBeExcluded := func(item string) bool {
+ excluded := []string{
+ "OnActivate",
+ "Implemented",
+ "LoadPluginConfiguration",
+ "ServeHTTP",
+ }
+ for _, exclusion := range excluded {
+ if exclusion == item {
+ return true
+ }
+ }
+ return false
+ }
+ hooksResult := make([]IHookEntry, 0, len(info.Hooks))
+ for _, hook := range info.Hooks {
+ if !toBeExcluded(hook.FuncName) {
+ hooksResult = append(hooksResult, hook)
+ }
+ }
+ info.Hooks = hooksResult
+
+ apiResult := make([]IHookEntry, 0, len(info.API))
+ for _, api := range info.API {
+ if !toBeExcluded(api.FuncName) {
+ apiResult = append(apiResult, api)
+ }
+ }
+ info.API = apiResult
+
+ return info
+}
+
+func main() {
+ pluginPackageDir := getPluginPackageDir()
+
+ log.Println("Generating plugin glue")
+ info, err := getPluginInfo(pluginPackageDir)
+ if err != nil {
+ fmt.Println("Unable to get plugin info: " + err.Error())
+ }
+
+ info = removeExcluded(info)
+
+ generateGlue(info)
+}
diff --git a/plugin/rpcplugin/io.go b/plugin/io_rpc.go
index 44b89956c..7bb86b52b 100644
--- a/plugin/rpcplugin/io.go
+++ b/plugin/io_rpc.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
+// See LICENSE.txt for license information.
-package rpcplugin
+package plugin
import (
"bufio"
diff --git a/plugin/plugin.go b/plugin/plugin.go
deleted file mode 100644
index 3150bf56a..000000000
--- a/plugin/plugin.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-// The plugin package defines the primary interfaces for interacting with a Mattermost server: the
-// API and the hook interfaces.
-//
-// The API interface is used to perform actions. The Hook interface is used to respond to actions.
-//
-// Plugins should define a type that implements some of the methods from the Hook interface, then
-// pass an instance of that object into the rpcplugin package's Main function (See the HelloWorld
-// example.).
-//
-// Testing
-//
-// To make testing plugins easier, you can use the plugintest package to create a mock API for your
-// plugin to interact with. See
-// https://godoc.org/github.com/mattermost/mattermost-server/plugin/plugintest
-package plugin
diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go
deleted file mode 100644
index f704aa5bb..000000000
--- a/plugin/pluginenv/environment.go
+++ /dev/null
@@ -1,396 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-// Package pluginenv provides high level functionality for discovering and launching plugins.
-package pluginenv
-
-import (
- "fmt"
- "io/ioutil"
- "net/http"
- "path/filepath"
- "sync"
-
- "github.com/pkg/errors"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
-)
-
-type APIProviderFunc func(*model.Manifest) (plugin.API, error)
-type SupervisorProviderFunc func(*model.BundleInfo) (plugin.Supervisor, error)
-
-type ActivePlugin struct {
- BundleInfo *model.BundleInfo
- Supervisor plugin.Supervisor
-}
-
-// Environment represents an environment that plugins are discovered and launched in.
-type Environment struct {
- searchPath string
- webappPath string
- apiProvider APIProviderFunc
- supervisorProvider SupervisorProviderFunc
- activePlugins map[string]ActivePlugin
- mutex sync.RWMutex
-}
-
-type Option func(*Environment)
-
-// Creates a new environment. At a minimum, the APIProvider and SearchPath options are required.
-func New(options ...Option) (*Environment, error) {
- env := &Environment{
- activePlugins: make(map[string]ActivePlugin),
- }
- for _, opt := range options {
- opt(env)
- }
- if env.supervisorProvider == nil {
- env.supervisorProvider = DefaultSupervisorProvider
- }
- if env.searchPath == "" {
- return nil, fmt.Errorf("a search path must be provided")
- }
- return env, nil
-}
-
-// Returns the configured webapp path.
-func (env *Environment) WebappPath() string {
- return env.webappPath
-}
-
-// Returns the configured search path.
-func (env *Environment) SearchPath() string {
- return env.searchPath
-}
-
-// Returns a list of all plugins found within the environment.
-func (env *Environment) Plugins() ([]*model.BundleInfo, error) {
- return ScanSearchPath(env.searchPath)
-}
-
-// Returns a list of all currently active plugins within the environment.
-func (env *Environment) ActivePlugins() []*model.BundleInfo {
- env.mutex.RLock()
- defer env.mutex.RUnlock()
-
- activePlugins := []*model.BundleInfo{}
- for _, p := range env.activePlugins {
- activePlugins = append(activePlugins, p.BundleInfo)
- }
-
- return activePlugins
-}
-
-// Returns the ids of the currently active plugins.
-func (env *Environment) ActivePluginIds() (ids []string) {
- env.mutex.RLock()
- defer env.mutex.RUnlock()
-
- for id := range env.activePlugins {
- ids = append(ids, id)
- }
- return
-}
-
-// Returns true if the plugin is active, false otherwise.
-func (env *Environment) IsPluginActive(pluginId string) bool {
- env.mutex.RLock()
- defer env.mutex.RUnlock()
-
- for id := range env.activePlugins {
- if id == pluginId {
- return true
- }
- }
-
- return false
-}
-
-// Activates the plugin with the given id.
-func (env *Environment) ActivatePlugin(id string, onError func(error)) error {
- env.mutex.Lock()
- defer env.mutex.Unlock()
-
- if !plugin.IsValidId(id) {
- return fmt.Errorf("invalid plugin id: %s", id)
- }
-
- if _, ok := env.activePlugins[id]; ok {
- return fmt.Errorf("plugin already active: %v", id)
- }
- plugins, err := ScanSearchPath(env.searchPath)
- if err != nil {
- return err
- }
- var bundle *model.BundleInfo
- for _, p := range plugins {
- if p.Manifest != nil && p.Manifest.Id == id {
- if bundle != nil {
- return fmt.Errorf("multiple plugins found: %v", id)
- }
- bundle = p
- }
- }
- if bundle == nil {
- return fmt.Errorf("plugin not found: %v", id)
- }
-
- activePlugin := ActivePlugin{BundleInfo: bundle}
-
- var supervisor plugin.Supervisor
-
- if bundle.Manifest.Backend != nil {
- if env.apiProvider == nil {
- return fmt.Errorf("env missing api provider, cannot activate plugin: %v", id)
- }
-
- supervisor, err = env.supervisorProvider(bundle)
- if err != nil {
- return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id)
- }
- api, err := env.apiProvider(bundle.Manifest)
- if err != nil {
- return errors.Wrapf(err, "unable to get api for plugin: %v", id)
- }
- if err := supervisor.Start(api); err != nil {
- return errors.Wrapf(err, "unable to start plugin: %v", id)
- }
- if onError != nil {
- go func() {
- err := supervisor.Wait()
- if err != nil {
- onError(err)
- }
- }()
- }
-
- activePlugin.Supervisor = supervisor
- }
-
- if bundle.Manifest.Webapp != nil {
- if env.webappPath == "" {
- if supervisor != nil {
- supervisor.Stop()
- }
- return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id)
- }
-
- bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath)
- if bundlePath == "" || bundlePath[0] == '.' {
- return fmt.Errorf("invalid webapp bundle path")
- }
- bundlePath = filepath.Join(env.searchPath, id, bundlePath)
-
- webappBundle, err := ioutil.ReadFile(bundlePath)
- if err != nil {
- // Backwards compatibility for plugins where webapp.bundle_path was ignored. This should
- // be removed eventually.
- if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil {
- webappBundle = webappBundle2
- } else {
- if supervisor != nil {
- supervisor.Stop()
- }
- return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
- }
- }
-
- err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644)
- if err != nil {
- if supervisor != nil {
- supervisor.Stop()
- }
- return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
- }
- }
-
- env.activePlugins[id] = activePlugin
- return nil
-}
-
-// Deactivates the plugin with the given id.
-func (env *Environment) DeactivatePlugin(id string) error {
- env.mutex.Lock()
- defer env.mutex.Unlock()
-
- if activePlugin, ok := env.activePlugins[id]; !ok {
- return fmt.Errorf("plugin not active: %v", id)
- } else {
- delete(env.activePlugins, id)
- var err error
- if activePlugin.Supervisor != nil {
- err = activePlugin.Supervisor.Hooks().OnDeactivate()
- if serr := activePlugin.Supervisor.Stop(); err == nil {
- err = serr
- }
- }
- return err
- }
-}
-
-// Deactivates all plugins and gracefully shuts down the environment.
-func (env *Environment) Shutdown() (errs []error) {
- env.mutex.Lock()
- defer env.mutex.Unlock()
-
- for _, activePlugin := range env.activePlugins {
- if activePlugin.Supervisor != nil {
- if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
- errs = append(errs, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id))
- }
- if err := activePlugin.Supervisor.Stop(); err != nil {
- errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id))
- }
- }
- }
- env.activePlugins = make(map[string]ActivePlugin)
- return
-}
-
-type MultiPluginHooks struct {
- env *Environment
-}
-
-type SinglePluginHooks struct {
- env *Environment
- pluginId string
-}
-
-func (env *Environment) Hooks() *MultiPluginHooks {
- return &MultiPluginHooks{
- env: env,
- }
-}
-
-func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks {
- return &SinglePluginHooks{
- env: env,
- pluginId: id,
- }
-}
-
-func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) {
- h.env.mutex.RLock()
- defer h.env.mutex.RUnlock()
-
- for _, activePlugin := range h.env.activePlugins {
- if activePlugin.Supervisor == nil {
- continue
- }
- if err := f(activePlugin.Supervisor.Hooks()); err != nil {
- errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
- }
- }
- return
-}
-
-// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
-// encountered will be returned.
-func (h *MultiPluginHooks) OnConfigurationChange() []error {
- return h.invoke(func(hooks plugin.Hooks) error {
- if err := hooks.OnConfigurationChange(); err != nil {
- return errors.Wrapf(err, "error calling OnConfigurationChange hook")
- }
- return nil
- })
-}
-
-// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
-// 404 not found.
-//
-// It expects the request's context to have a plugin_id set.
-func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if id := r.Context().Value("plugin_id"); id != nil {
- if idstr, ok := id.(string); ok {
- h.env.mutex.RLock()
- defer h.env.mutex.RUnlock()
- if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil {
- plugin.Supervisor.Hooks().ServeHTTP(w, r)
- return
- }
- }
- }
- http.NotFound(w, r)
-}
-
-// MessageWillBePosted invokes the MessageWillBePosted hook for all plugins. Ordering
-// is not guaranteed and the next plugin will get the previous one's modifications.
-// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made.
-// Returns the final result post, or nil if the post was rejected and a string with a reason
-// for the user the message was rejected.
-func (h *MultiPluginHooks) MessageWillBePosted(post *model.Post) (*model.Post, string) {
- h.env.mutex.RLock()
- defer h.env.mutex.RUnlock()
-
- for _, activePlugin := range h.env.activePlugins {
- if activePlugin.Supervisor == nil {
- continue
- }
- var rejectionReason string
- post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBePosted(post)
- if post == nil {
- return nil, rejectionReason
- }
- }
- return post, ""
-}
-
-// MessageWillBeUpdated invokes the MessageWillBeUpdated hook for all plugins. Ordering
-// is not guaranteed and the next plugin will get the previous one's modifications.
-// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made.
-// Returns the final result post, or nil if the post was rejected and a string with a reason
-// for the user the message was rejected.
-func (h *MultiPluginHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) {
- h.env.mutex.RLock()
- defer h.env.mutex.RUnlock()
-
- post := newPost
- for _, activePlugin := range h.env.activePlugins {
- if activePlugin.Supervisor == nil {
- continue
- }
- var rejectionReason string
- post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBeUpdated(post, oldPost)
- if post == nil {
- return nil, rejectionReason
- }
- }
- return post, ""
-}
-
-func (h *MultiPluginHooks) MessageHasBeenPosted(post *model.Post) {
- h.invoke(func(hooks plugin.Hooks) error {
- hooks.MessageHasBeenPosted(post)
- return nil
- })
-}
-
-func (h *MultiPluginHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) {
- h.invoke(func(hooks plugin.Hooks) error {
- hooks.MessageHasBeenUpdated(newPost, oldPost)
- return nil
- })
-}
-
-func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error {
- h.env.mutex.RLock()
- defer h.env.mutex.RUnlock()
-
- if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil {
- if err := f(activePlugin.Supervisor.Hooks()); err != nil {
- return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id)
- }
- return nil
- }
- return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId)
-}
-
-// ExecuteCommand invokes the ExecuteCommand hook for the plugin.
-func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) {
- err = h.invoke(func(hooks plugin.Hooks) error {
- resp, appErr = hooks.ExecuteCommand(args)
- return nil
- })
- return
-}
diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go
deleted file mode 100644
index 8c1397799..000000000
--- a/plugin/pluginenv/environment_test.go
+++ /dev/null
@@ -1,409 +0,0 @@
-package pluginenv
-
-import (
- "context"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
-)
-
-type MockProvider struct {
- mock.Mock
-}
-
-func (m *MockProvider) API(manifest *model.Manifest) (plugin.API, error) {
- ret := m.Called()
- if ret.Get(0) == nil {
- return nil, ret.Error(1)
- }
- return ret.Get(0).(plugin.API), ret.Error(1)
-}
-
-func (m *MockProvider) Supervisor(bundle *model.BundleInfo) (plugin.Supervisor, error) {
- ret := m.Called()
- if ret.Get(0) == nil {
- return nil, ret.Error(1)
- }
- return ret.Get(0).(plugin.Supervisor), ret.Error(1)
-}
-
-type MockSupervisor struct {
- mock.Mock
-}
-
-func (m *MockSupervisor) Start(api plugin.API) error {
- return m.Called(api).Error(0)
-}
-
-func (m *MockSupervisor) Stop() error {
- return m.Called().Error(0)
-}
-
-func (m *MockSupervisor) Hooks() plugin.Hooks {
- return m.Called().Get(0).(plugin.Hooks)
-}
-
-func (m *MockSupervisor) Wait() error {
- return m.Called().Get(0).(error)
-}
-
-func initTmpDir(t *testing.T, files map[string]string) string {
- success := false
- dir, err := ioutil.TempDir("", "mm-plugin-test")
- require.NoError(t, err)
- defer func() {
- if !success {
- os.RemoveAll(dir)
- }
- }()
-
- for name, contents := range files {
- path := filepath.Join(dir, name)
- parent := filepath.Dir(path)
- require.NoError(t, os.MkdirAll(parent, 0700))
- f, err := os.Create(path)
- require.NoError(t, err)
- _, err = f.WriteString(contents)
- f.Close()
- require.NoError(t, err)
- }
-
- success = true
- return dir
-}
-
-func TestNew_MissingOptions(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo"}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- APIProvider(provider.API),
- )
- assert.Nil(t, env)
- assert.Error(t, err)
-}
-
-func TestEnvironment(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- ".foo/plugin.json": `{"id": "foo"}`,
- "foo/bar": "asdf",
- "foo/plugin.json": `{"id": "foo", "backend": {}}`,
- "bar/zxc": "qwer",
- "baz/plugin.yaml": "id: baz",
- "bad/plugin.json": "asd",
- "qwe": "asd",
- })
- defer os.RemoveAll(dir)
-
- webappDir := "notarealdirectory"
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- SearchPath(dir),
- WebappPath(webappDir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- plugins, err := env.Plugins()
- assert.NoError(t, err)
- assert.Len(t, plugins, 3)
-
- activePlugins := env.ActivePlugins()
- assert.Len(t, activePlugins, 0)
-
- assert.Error(t, env.ActivatePlugin("x", nil))
-
- var api struct{ plugin.API }
- var supervisor MockSupervisor
- defer supervisor.AssertExpectations(t)
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- provider.On("API").Return(&api, nil)
- provider.On("Supervisor").Return(&supervisor, nil)
-
- supervisor.On("Start", &api).Return(nil)
- supervisor.On("Stop").Return(nil)
- supervisor.On("Hooks").Return(&hooks)
-
- assert.NoError(t, env.ActivatePlugin("foo", nil))
- assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
- activePlugins = env.ActivePlugins()
- assert.Len(t, activePlugins, 1)
- assert.Error(t, env.ActivatePlugin("foo", nil))
- assert.True(t, env.IsPluginActive("foo"))
-
- hooks.On("OnDeactivate").Return(nil)
- assert.NoError(t, env.DeactivatePlugin("foo"))
- assert.Error(t, env.DeactivatePlugin("foo"))
- assert.False(t, env.IsPluginActive("foo"))
-
- assert.NoError(t, env.ActivatePlugin("foo", nil))
- assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
-
- assert.Equal(t, env.SearchPath(), dir)
- assert.Equal(t, env.WebappPath(), webappDir)
-
- assert.Empty(t, env.Shutdown())
-}
-
-func TestEnvironment_DuplicatePluginError(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo"}`,
- "foo2/plugin.json": `{"id": "foo"}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- SearchPath(dir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- assert.Error(t, env.ActivatePlugin("foo", nil))
- assert.Empty(t, env.ActivePluginIds())
-}
-
-func TestEnvironment_BadSearchPathError(t *testing.T) {
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- SearchPath("thissearchpathshouldnotexist!"),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- assert.Error(t, env.ActivatePlugin("foo", nil))
- assert.Empty(t, env.ActivePluginIds())
-}
-
-func TestEnvironment_ActivatePluginErrors(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo", "backend": {}}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
-
- env, err := New(
- SearchPath(dir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- var api struct{ plugin.API }
- var supervisor MockSupervisor
- var hooks plugintest.Hooks
-
- for name, setup := range map[string]func(){
- "SupervisorProviderError": func() {
- provider.On("Supervisor").Return(nil, fmt.Errorf("test error"))
- },
- "APIProviderError": func() {
- provider.On("API").Return(plugin.API(nil), fmt.Errorf("test error"))
- provider.On("Supervisor").Return(&supervisor, nil)
- },
- "SupervisorError": func() {
- provider.On("API").Return(&api, nil)
- provider.On("Supervisor").Return(&supervisor, nil)
-
- supervisor.On("Start", &api).Return(fmt.Errorf("test error"))
- },
- } {
- t.Run(name, func(t *testing.T) {
- supervisor.Mock = mock.Mock{}
- hooks.Mock = mock.Mock{}
- provider.Mock = mock.Mock{}
- setup()
- assert.Error(t, env.ActivatePlugin("foo", nil))
- assert.Empty(t, env.ActivePluginIds())
- supervisor.AssertExpectations(t)
- hooks.AssertExpectations(t)
- provider.AssertExpectations(t)
- })
- }
-}
-
-func TestEnvironment_ShutdownError(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo", "backend": {}}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- SearchPath(dir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- var api struct{ plugin.API }
- var supervisor MockSupervisor
- defer supervisor.AssertExpectations(t)
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- provider.On("API").Return(&api, nil)
- provider.On("Supervisor").Return(&supervisor, nil)
-
- supervisor.On("Start", &api).Return(nil)
- supervisor.On("Stop").Return(fmt.Errorf("test error"))
- supervisor.On("Hooks").Return(&hooks)
-
- hooks.On("OnDeactivate").Return(fmt.Errorf("test error"))
-
- assert.NoError(t, env.ActivatePlugin("foo", nil))
- assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
- assert.Len(t, env.Shutdown(), 2)
-}
-
-func TestEnvironment_ConcurrentHookInvocations(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo", "backend": {}}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- var api struct{ plugin.API }
- var supervisor MockSupervisor
- defer supervisor.AssertExpectations(t)
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- env, err := New(
- SearchPath(dir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- provider.On("API").Return(&api, nil)
- provider.On("Supervisor").Return(&supervisor, nil)
-
- supervisor.On("Start", &api).Return(nil)
- supervisor.On("Stop").Return(nil)
- supervisor.On("Hooks").Return(&hooks)
-
- ch := make(chan bool)
-
- hooks.On("OnDeactivate").Return(nil)
- hooks.On("ServeHTTP", mock.AnythingOfType("*httptest.ResponseRecorder"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
- r := args.Get(1).(*http.Request)
- if r.URL.Path == "/1" {
- <-ch
- } else {
- ch <- true
- }
- })
-
- assert.NoError(t, env.ActivatePlugin("foo", nil))
-
- rec := httptest.NewRecorder()
-
- wg := sync.WaitGroup{}
- wg.Add(2)
-
- go func() {
- req, err := http.NewRequest("GET", "/1", nil)
- require.NoError(t, err)
- env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo")))
- wg.Done()
- }()
-
- go func() {
- req, err := http.NewRequest("GET", "/2", nil)
- require.NoError(t, err)
- env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo")))
- wg.Done()
- }()
-
- wg.Wait()
-}
-
-func TestEnvironment_HooksForPlugins(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- "foo/plugin.json": `{"id": "foo", "backend": {}}`,
- })
- defer os.RemoveAll(dir)
-
- var provider MockProvider
- defer provider.AssertExpectations(t)
-
- env, err := New(
- SearchPath(dir),
- APIProvider(provider.API),
- SupervisorProvider(provider.Supervisor),
- )
- require.NoError(t, err)
- defer env.Shutdown()
-
- var api struct{ plugin.API }
- var supervisor MockSupervisor
- defer supervisor.AssertExpectations(t)
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- provider.On("API").Return(&api, nil)
- provider.On("Supervisor").Return(&supervisor, nil)
-
- supervisor.On("Start", &api).Return(nil)
- supervisor.On("Stop").Return(nil)
- supervisor.On("Hooks").Return(&hooks)
-
- hooks.On("OnDeactivate").Return(nil)
- hooks.On("ExecuteCommand", mock.AnythingOfType("*model.CommandArgs")).Return(&model.CommandResponse{
- Text: "bar",
- }, nil)
-
- assert.NoError(t, env.ActivatePlugin("foo", nil))
- assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
-
- resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{
- Command: "/foo",
- })
- assert.Equal(t, "bar", resp.Text)
- assert.Nil(t, appErr)
- assert.NoError(t, err)
-
- assert.Empty(t, env.Shutdown())
-}
diff --git a/plugin/pluginenv/options.go b/plugin/pluginenv/options.go
deleted file mode 100644
index 43cbdac68..000000000
--- a/plugin/pluginenv/options.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package pluginenv
-
-import (
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox"
-)
-
-// APIProvider specifies a function that provides an API implementation to each plugin.
-func APIProvider(provider APIProviderFunc) Option {
- return func(env *Environment) {
- env.apiProvider = provider
- }
-}
-
-// SupervisorProvider specifies a function that provides a Supervisor implementation to each plugin.
-// If unspecified, DefaultSupervisorProvider is used.
-func SupervisorProvider(provider SupervisorProviderFunc) Option {
- return func(env *Environment) {
- env.supervisorProvider = provider
- }
-}
-
-// SearchPath specifies a directory that contains the plugins to launch.
-func SearchPath(path string) Option {
- return func(env *Environment) {
- env.searchPath = path
- }
-}
-
-// WebappPath specifies the static directory serving the webapp.
-func WebappPath(path string) Option {
- return func(env *Environment) {
- env.webappPath = path
- }
-}
-
-// DefaultSupervisorProvider chooses a supervisor based on the system and the plugin's manifest
-// contents. E.g. if the manifest specifies a backend executable, it will be given an
-// rpcplugin.Supervisor.
-func DefaultSupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
- if err := sandbox.CheckSupport(); err == nil {
- return sandbox.SupervisorProvider(bundle)
- }
- return rpcplugin.SupervisorProvider(bundle)
-}
diff --git a/plugin/pluginenv/options_test.go b/plugin/pluginenv/options_test.go
deleted file mode 100644
index 01fa206d0..000000000
--- a/plugin/pluginenv/options_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package pluginenv
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-func TestDefaultSupervisorProvider(t *testing.T) {
- _, err := DefaultSupervisorProvider(&model.BundleInfo{})
- assert.Error(t, err)
-
- _, err = DefaultSupervisorProvider(&model.BundleInfo{
- Manifest: &model.Manifest{},
- })
- assert.Error(t, err)
-
- supervisor, err := DefaultSupervisorProvider(&model.BundleInfo{
- Manifest: &model.Manifest{
- Backend: &model.ManifestBackend{
- Executable: "foo",
- },
- },
- })
- require.NoError(t, err)
- _, ok := supervisor.(*rpcplugin.Supervisor)
- assert.True(t, ok)
-}
diff --git a/plugin/pluginenv/search_path.go b/plugin/pluginenv/search_path.go
deleted file mode 100644
index 698424332..000000000
--- a/plugin/pluginenv/search_path.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package pluginenv
-
-import (
- "io/ioutil"
- "path/filepath"
-
- "github.com/mattermost/mattermost-server/model"
-)
-
-// 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
-}
diff --git a/plugin/pluginenv/search_path_test.go b/plugin/pluginenv/search_path_test.go
deleted file mode 100644
index dd9ff9a6b..000000000
--- a/plugin/pluginenv/search_path_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package pluginenv
-
-import (
- "encoding/json"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
-)
-
-func TestScanSearchPath(t *testing.T) {
- dir := initTmpDir(t, map[string]string{
- ".foo/plugin.json": `{"id": "foo"}`,
- "foo/bar": "asdf",
- "foo/plugin.json": `{"id": "foo"}`,
- "bar/zxc": "qwer",
- "baz/plugin.yaml": "id: baz",
- "bad/plugin.json": "asd",
- "qwe": "asd",
- })
- defer os.RemoveAll(dir)
-
- plugins, err := ScanSearchPath(dir)
- require.NoError(t, err)
- assert.Len(t, plugins, 3)
- assert.Contains(t, plugins, &model.BundleInfo{
- Path: filepath.Join(dir, "foo"),
- ManifestPath: filepath.Join(dir, "foo", "plugin.json"),
- Manifest: &model.Manifest{
- Id: "foo",
- },
- })
- assert.Contains(t, plugins, &model.BundleInfo{
- Path: filepath.Join(dir, "baz"),
- ManifestPath: filepath.Join(dir, "baz", "plugin.yaml"),
- Manifest: &model.Manifest{
- Id: "baz",
- },
- })
- foundError := false
- for _, x := range plugins {
- if x.ManifestError != nil {
- assert.Equal(t, x.Path, filepath.Join(dir, "bad"))
- assert.Equal(t, x.ManifestPath, filepath.Join(dir, "bad", "plugin.json"))
- syntexError, ok := x.ManifestError.(*json.SyntaxError)
- assert.True(t, ok)
- assert.EqualValues(t, 1, syntexError.Offset)
- foundError = true
- }
- }
- assert.True(t, foundError)
-}
-
-func TestScanSearchPath_Error(t *testing.T) {
- plugins, err := ScanSearchPath("not a valid path!")
- assert.Nil(t, plugins)
- assert.Error(t, err)
-}
diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go
index f1281a5ff..11c5a9c59 100644
--- a/plugin/plugintest/api.go
+++ b/plugin/plugintest/api.go
@@ -6,15 +6,14 @@ package plugintest
import mock "github.com/stretchr/testify/mock"
import model "github.com/mattermost/mattermost-server/model"
-import plugin "github.com/mattermost/mattermost-server/plugin"
-// APIMOCKINTERNAL is an autogenerated mock type for the APIMOCKINTERNAL type
-type APIMOCKINTERNAL struct {
+// API is an autogenerated mock type for the API type
+type API struct {
mock.Mock
}
// AddChannelMember provides a mock function with given fields: channelId, userId
-func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+func (_m *API) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
ret := _m.Called(channelId, userId)
var r0 *model.ChannelMember
@@ -39,7 +38,7 @@ func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*m
}
// CreateChannel provides a mock function with given fields: channel
-func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+func (_m *API) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
ret := _m.Called(channel)
var r0 *model.Channel
@@ -64,7 +63,7 @@ func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel
}
// CreatePost provides a mock function with given fields: post
-func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
+func (_m *API) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
ret := _m.Called(post)
var r0 *model.Post
@@ -89,7 +88,7 @@ func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.App
}
// CreateTeam provides a mock function with given fields: team
-func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
+func (_m *API) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
ret := _m.Called(team)
var r0 *model.Team
@@ -114,7 +113,7 @@ func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.App
}
// CreateUser provides a mock function with given fields: user
-func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.AppError) {
+func (_m *API) CreateUser(user *model.User) (*model.User, *model.AppError) {
ret := _m.Called(user)
var r0 *model.User
@@ -139,7 +138,7 @@ func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.App
}
// DeleteChannel provides a mock function with given fields: channelId
-func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError {
+func (_m *API) DeleteChannel(channelId string) *model.AppError {
ret := _m.Called(channelId)
var r0 *model.AppError
@@ -155,7 +154,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError {
}
// DeleteChannelMember provides a mock function with given fields: channelId, userId
-func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string) *model.AppError {
+func (_m *API) DeleteChannelMember(channelId string, userId string) *model.AppError {
ret := _m.Called(channelId, userId)
var r0 *model.AppError
@@ -171,7 +170,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string)
}
// DeletePost provides a mock function with given fields: postId
-func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError {
+func (_m *API) DeletePost(postId string) *model.AppError {
ret := _m.Called(postId)
var r0 *model.AppError
@@ -187,7 +186,7 @@ func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError {
}
// DeleteTeam provides a mock function with given fields: teamId
-func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError {
+func (_m *API) DeleteTeam(teamId string) *model.AppError {
ret := _m.Called(teamId)
var r0 *model.AppError
@@ -203,7 +202,7 @@ func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError {
}
// DeleteUser provides a mock function with given fields: userId
-func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError {
+func (_m *API) DeleteUser(userId string) *model.AppError {
ret := _m.Called(userId)
var r0 *model.AppError
@@ -219,7 +218,7 @@ func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError {
}
// GetChannel provides a mock function with given fields: channelId
-func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model.AppError) {
+func (_m *API) GetChannel(channelId string) (*model.Channel, *model.AppError) {
ret := _m.Called(channelId)
var r0 *model.Channel
@@ -244,7 +243,7 @@ func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model.
}
// GetChannelByName provides a mock function with given fields: name, teamId
-func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) {
+func (_m *API) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) {
ret := _m.Called(name, teamId)
var r0 *model.Channel
@@ -269,7 +268,7 @@ func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model.
}
// GetChannelMember provides a mock function with given fields: channelId, userId
-func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+func (_m *API) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
ret := _m.Called(channelId, userId)
var r0 *model.ChannelMember
@@ -294,7 +293,7 @@ func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*m
}
// GetDirectChannel provides a mock function with given fields: userId1, userId2
-func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
+func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
ret := _m.Called(userId1, userId2)
var r0 *model.Channel
@@ -319,7 +318,7 @@ func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*mo
}
// GetGroupChannel provides a mock function with given fields: userIds
-func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
+func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
ret := _m.Called(userIds)
var r0 *model.Channel
@@ -344,7 +343,7 @@ func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *m
}
// GetPost provides a mock function with given fields: postId
-func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError) {
+func (_m *API) GetPost(postId string) (*model.Post, *model.AppError) {
ret := _m.Called(postId)
var r0 *model.Post
@@ -369,7 +368,7 @@ func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError)
}
// GetTeam provides a mock function with given fields: teamId
-func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError) {
+func (_m *API) GetTeam(teamId string) (*model.Team, *model.AppError) {
ret := _m.Called(teamId)
var r0 *model.Team
@@ -394,7 +393,7 @@ func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError)
}
// GetTeamByName provides a mock function with given fields: name
-func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppError) {
+func (_m *API) GetTeamByName(name string) (*model.Team, *model.AppError) {
ret := _m.Called(name)
var r0 *model.Team
@@ -419,7 +418,7 @@ func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppEr
}
// GetUser provides a mock function with given fields: userId
-func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError) {
+func (_m *API) GetUser(userId string) (*model.User, *model.AppError) {
ret := _m.Called(userId)
var r0 *model.User
@@ -444,7 +443,7 @@ func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError)
}
// GetUserByEmail provides a mock function with given fields: email
-func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.AppError) {
+func (_m *API) GetUserByEmail(email string) (*model.User, *model.AppError) {
ret := _m.Called(email)
var r0 *model.User
@@ -469,7 +468,7 @@ func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.App
}
// GetUserByUsername provides a mock function with given fields: name
-func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.AppError) {
+func (_m *API) GetUserByUsername(name string) (*model.User, *model.AppError) {
ret := _m.Called(name)
var r0 *model.User
@@ -493,16 +492,57 @@ func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.A
return r0, r1
}
-// KeyValueStore provides a mock function with given fields:
-func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore {
- ret := _m.Called()
+// KVDelete provides a mock function with given fields: key
+func (_m *API) KVDelete(key string) *model.AppError {
+ ret := _m.Called(key)
- var r0 plugin.KeyValueStore
- if rf, ok := ret.Get(0).(func() plugin.KeyValueStore); ok {
- r0 = rf()
+ var r0 *model.AppError
+ if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
+ r0 = rf(key)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*model.AppError)
+ }
+ }
+
+ return r0
+}
+
+// KVGet provides a mock function with given fields: key
+func (_m *API) KVGet(key string) ([]byte, *model.AppError) {
+ ret := _m.Called(key)
+
+ var r0 []byte
+ if rf, ok := ret.Get(0).(func(string) []byte); ok {
+ r0 = rf(key)
} else {
if ret.Get(0) != nil {
- r0 = ret.Get(0).(plugin.KeyValueStore)
+ r0 = ret.Get(0).([]byte)
+ }
+ }
+
+ var r1 *model.AppError
+ if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
+ r1 = rf(key)
+ } else {
+ if ret.Get(1) != nil {
+ r1 = ret.Get(1).(*model.AppError)
+ }
+ }
+
+ return r0, r1
+}
+
+// KVSet provides a mock function with given fields: key, value
+func (_m *API) KVSet(key string, value []byte) *model.AppError {
+ ret := _m.Called(key, value)
+
+ var r0 *model.AppError
+ if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
+ r0 = rf(key, value)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*model.AppError)
}
}
@@ -510,7 +550,7 @@ func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore {
}
// LoadPluginConfiguration provides a mock function with given fields: dest
-func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error {
+func (_m *API) LoadPluginConfiguration(dest interface{}) error {
ret := _m.Called(dest)
var r0 error
@@ -524,7 +564,7 @@ func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error {
}
// RegisterCommand provides a mock function with given fields: command
-func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error {
+func (_m *API) RegisterCommand(command *model.Command) error {
ret := _m.Called(command)
var r0 error
@@ -538,7 +578,7 @@ func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error {
}
// UnregisterCommand provides a mock function with given fields: teamId, trigger
-func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) error {
+func (_m *API) UnregisterCommand(teamId string, trigger string) error {
ret := _m.Called(teamId, trigger)
var r0 error
@@ -552,7 +592,7 @@ func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) erro
}
// UpdateChannel provides a mock function with given fields: channel
-func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+func (_m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
ret := _m.Called(channel)
var r0 *model.Channel
@@ -577,7 +617,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel
}
// UpdateChannelMemberNotifications provides a mock function with given fields: channelId, userId, notifications
-func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+func (_m *API) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
ret := _m.Called(channelId, userId, notifications)
var r0 *model.ChannelMember
@@ -602,7 +642,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, us
}
// UpdateChannelMemberRoles provides a mock function with given fields: channelId, userId, newRoles
-func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
+func (_m *API) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
ret := _m.Called(channelId, userId, newRoles)
var r0 *model.ChannelMember
@@ -627,7 +667,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId str
}
// UpdatePost provides a mock function with given fields: post
-func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
+func (_m *API) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
ret := _m.Called(post)
var r0 *model.Post
@@ -652,7 +692,7 @@ func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.App
}
// UpdateTeam provides a mock function with given fields: team
-func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
+func (_m *API) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
ret := _m.Called(team)
var r0 *model.Team
@@ -677,7 +717,7 @@ func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.App
}
// UpdateUser provides a mock function with given fields: user
-func (_m *APIMOCKINTERNAL) UpdateUser(user *model.User) (*model.User, *model.AppError) {
+func (_m *API) UpdateUser(user *model.User) (*model.User, *model.AppError) {
ret := _m.Called(user)
var r0 *model.User
diff --git a/plugin/plugintest/apioverride.go b/plugin/plugintest/apioverride.go
deleted file mode 100644
index 54cfe27bc..000000000
--- a/plugin/plugintest/apioverride.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-package plugintest
-
-import "github.com/mattermost/mattermost-server/plugin"
-
-type API struct {
- APIMOCKINTERNAL
- Store *KeyValueStore
-}
-
-var _ plugin.API = (*API)(nil)
-var _ plugin.KeyValueStore = (*KeyValueStore)(nil)
-
-func (m *API) KeyValueStore() plugin.KeyValueStore {
- return m.Store
-}
diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go
index 3de257c76..790a5a993 100644
--- a/plugin/plugintest/hooks.go
+++ b/plugin/plugintest/hooks.go
@@ -7,7 +7,6 @@ package plugintest
import http "net/http"
import mock "github.com/stretchr/testify/mock"
import model "github.com/mattermost/mattermost-server/model"
-import plugin "github.com/mattermost/mattermost-server/plugin"
// Hooks is an autogenerated mock type for the Hooks type
type Hooks struct {
@@ -39,6 +38,29 @@ func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse
return r0, r1
}
+// Implemented provides a mock function with given fields:
+func (_m *Hooks) Implemented() ([]string, error) {
+ ret := _m.Called()
+
+ var r0 []string
+ if rf, ok := ret.Get(0).(func() []string); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]string)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func() error); ok {
+ r1 = rf()
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// MessageHasBeenPosted provides a mock function with given fields: post
func (_m *Hooks) MessageHasBeenPosted(post *model.Post) {
_m.Called(post)
@@ -95,13 +117,13 @@ func (_m *Hooks) MessageWillBeUpdated(newPost *model.Post, oldPost *model.Post)
return r0, r1
}
-// OnActivate provides a mock function with given fields: _a0
-func (_m *Hooks) OnActivate(_a0 plugin.API) error {
- ret := _m.Called(_a0)
+// OnActivate provides a mock function with given fields:
+func (_m *Hooks) OnActivate() error {
+ ret := _m.Called()
var r0 error
- if rf, ok := ret.Get(0).(func(plugin.API) error); ok {
- r0 = rf(_a0)
+ if rf, ok := ret.Get(0).(func() error); ok {
+ r0 = rf()
} else {
r0 = ret.Error(0)
}
@@ -137,7 +159,7 @@ func (_m *Hooks) OnDeactivate() error {
return r0
}
-// ServeHTTP provides a mock function with given fields: _a0, _a1
-func (_m *Hooks) ServeHTTP(_a0 http.ResponseWriter, _a1 *http.Request) {
- _m.Called(_a0, _a1)
+// ServeHTTP provides a mock function with given fields: w, r
+func (_m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ _m.Called(w, r)
}
diff --git a/plugin/plugintest/key_value_store.go b/plugin/plugintest/key_value_store.go
deleted file mode 100644
index 30d60d708..000000000
--- a/plugin/plugintest/key_value_store.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Code generated by mockery v1.0.0. DO NOT EDIT.
-
-// Regenerate this file using `make plugin-mocks`.
-
-package plugintest
-
-import mock "github.com/stretchr/testify/mock"
-import model "github.com/mattermost/mattermost-server/model"
-
-// KeyValueStore is an autogenerated mock type for the KeyValueStore type
-type KeyValueStore struct {
- mock.Mock
-}
-
-// Delete provides a mock function with given fields: key
-func (_m *KeyValueStore) Delete(key string) *model.AppError {
- ret := _m.Called(key)
-
- var r0 *model.AppError
- if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
- r0 = rf(key)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(*model.AppError)
- }
- }
-
- return r0
-}
-
-// Get provides a mock function with given fields: key
-func (_m *KeyValueStore) Get(key string) ([]byte, *model.AppError) {
- ret := _m.Called(key)
-
- var r0 []byte
- if rf, ok := ret.Get(0).(func(string) []byte); ok {
- r0 = rf(key)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).([]byte)
- }
- }
-
- var r1 *model.AppError
- if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
- r1 = rf(key)
- } else {
- if ret.Get(1) != nil {
- r1 = ret.Get(1).(*model.AppError)
- }
- }
-
- return r0, r1
-}
-
-// Set provides a mock function with given fields: key, value
-func (_m *KeyValueStore) Set(key string, value []byte) *model.AppError {
- ret := _m.Called(key, value)
-
- var r0 *model.AppError
- if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
- r0 = rf(key, value)
- } else {
- if ret.Get(0) != nil {
- r0 = ret.Get(0).(*model.AppError)
- }
- }
-
- return r0
-}
diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go
deleted file mode 100644
index c81bbb7c5..000000000
--- a/plugin/rpcplugin/api.go
+++ /dev/null
@@ -1,718 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "encoding/gob"
- "encoding/json"
- "io"
- "net/http"
- "net/rpc"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
-)
-
-type LocalAPI struct {
- api plugin.API
- muxer *Muxer
-}
-
-func (api *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error {
- var config interface{}
- if err := api.api.LoadPluginConfiguration(&config); err != nil {
- return err
- }
- b, err := json.Marshal(config)
- if err != nil {
- return err
- }
- *reply = b
- return nil
-}
-
-func (api *LocalAPI) RegisterCommand(args *model.Command, reply *APITeamReply) error {
- return api.api.RegisterCommand(args)
-}
-
-func (api *LocalAPI) UnregisterCommand(args *APIUnregisterCommandArgs, reply *APITeamReply) error {
- return api.api.UnregisterCommand(args.TeamId, args.Trigger)
-}
-
-type APIErrorReply struct {
- Error *model.AppError
-}
-
-type APITeamReply struct {
- Team *model.Team
- Error *model.AppError
-}
-
-func (api *LocalAPI) CreateTeam(args *model.Team, reply *APITeamReply) error {
- team, err := api.api.CreateTeam(args)
- *reply = APITeamReply{
- Team: team,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) DeleteTeam(args string, reply *APIErrorReply) error {
- *reply = APIErrorReply{
- Error: api.api.DeleteTeam(args),
- }
- return nil
-}
-
-func (api *LocalAPI) GetTeam(args string, reply *APITeamReply) error {
- team, err := api.api.GetTeam(args)
- *reply = APITeamReply{
- Team: team,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetTeamByName(args string, reply *APITeamReply) error {
- team, err := api.api.GetTeamByName(args)
- *reply = APITeamReply{
- Team: team,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdateTeam(args *model.Team, reply *APITeamReply) error {
- team, err := api.api.UpdateTeam(args)
- *reply = APITeamReply{
- Team: team,
- Error: err,
- }
- return nil
-}
-
-type APIUserReply struct {
- User *model.User
- Error *model.AppError
-}
-
-func (api *LocalAPI) CreateUser(args *model.User, reply *APIUserReply) error {
- user, err := api.api.CreateUser(args)
- *reply = APIUserReply{
- User: user,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) DeleteUser(args string, reply *APIErrorReply) error {
- *reply = APIErrorReply{
- Error: api.api.DeleteUser(args),
- }
- return nil
-}
-
-func (api *LocalAPI) GetUser(args string, reply *APIUserReply) error {
- user, err := api.api.GetUser(args)
- *reply = APIUserReply{
- User: user,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetUserByEmail(args string, reply *APIUserReply) error {
- user, err := api.api.GetUserByEmail(args)
- *reply = APIUserReply{
- User: user,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetUserByUsername(args string, reply *APIUserReply) error {
- user, err := api.api.GetUserByUsername(args)
- *reply = APIUserReply{
- User: user,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdateUser(args *model.User, reply *APIUserReply) error {
- user, err := api.api.UpdateUser(args)
- *reply = APIUserReply{
- User: user,
- Error: err,
- }
- return nil
-}
-
-type APIGetChannelByNameArgs struct {
- Name string
- TeamId string
-}
-
-type APIGetDirectChannelArgs struct {
- UserId1 string
- UserId2 string
-}
-
-type APIGetGroupChannelArgs struct {
- UserIds []string
-}
-
-type APIAddChannelMemberArgs struct {
- ChannelId string
- UserId string
-}
-
-type APIGetChannelMemberArgs struct {
- ChannelId string
- UserId string
-}
-
-type APIUpdateChannelMemberRolesArgs struct {
- ChannelId string
- UserId string
- NewRoles string
-}
-
-type APIUpdateChannelMemberNotificationsArgs struct {
- ChannelId string
- UserId string
- Notifications map[string]string
-}
-
-type APIDeleteChannelMemberArgs struct {
- ChannelId string
- UserId string
-}
-
-type APIChannelReply struct {
- Channel *model.Channel
- Error *model.AppError
-}
-
-type APIChannelMemberReply struct {
- ChannelMember *model.ChannelMember
- Error *model.AppError
-}
-
-func (api *LocalAPI) CreateChannel(args *model.Channel, reply *APIChannelReply) error {
- channel, err := api.api.CreateChannel(args)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) DeleteChannel(args string, reply *APIErrorReply) error {
- *reply = APIErrorReply{
- Error: api.api.DeleteChannel(args),
- }
- return nil
-}
-
-func (api *LocalAPI) GetChannel(args string, reply *APIChannelReply) error {
- channel, err := api.api.GetChannel(args)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetChannelByName(args *APIGetChannelByNameArgs, reply *APIChannelReply) error {
- channel, err := api.api.GetChannelByName(args.Name, args.TeamId)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetDirectChannel(args *APIGetDirectChannelArgs, reply *APIChannelReply) error {
- channel, err := api.api.GetDirectChannel(args.UserId1, args.UserId2)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetGroupChannel(args *APIGetGroupChannelArgs, reply *APIChannelReply) error {
- channel, err := api.api.GetGroupChannel(args.UserIds)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdateChannel(args *model.Channel, reply *APIChannelReply) error {
- channel, err := api.api.UpdateChannel(args)
- *reply = APIChannelReply{
- Channel: channel,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) AddChannelMember(args *APIAddChannelMemberArgs, reply *APIChannelMemberReply) error {
- member, err := api.api.AddChannelMember(args.ChannelId, args.UserId)
- *reply = APIChannelMemberReply{
- ChannelMember: member,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIChannelMemberReply) error {
- member, err := api.api.GetChannelMember(args.ChannelId, args.UserId)
- *reply = APIChannelMemberReply{
- ChannelMember: member,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdateChannelMemberRoles(args *APIUpdateChannelMemberRolesArgs, reply *APIChannelMemberReply) error {
- member, err := api.api.UpdateChannelMemberRoles(args.ChannelId, args.UserId, args.NewRoles)
- *reply = APIChannelMemberReply{
- ChannelMember: member,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdateChannelMemberNotifications(args *APIUpdateChannelMemberNotificationsArgs, reply *APIChannelMemberReply) error {
- member, err := api.api.UpdateChannelMemberNotifications(args.ChannelId, args.UserId, args.Notifications)
- *reply = APIChannelMemberReply{
- ChannelMember: member,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) DeleteChannelMember(args *APIDeleteChannelMemberArgs, reply *APIErrorReply) error {
- err := api.api.DeleteChannelMember(args.ChannelId, args.UserId)
- *reply = APIErrorReply{
- Error: err,
- }
- return nil
-}
-
-type APIPostReply struct {
- Post *model.Post
- Error *model.AppError
-}
-
-func (api *LocalAPI) CreatePost(args *model.Post, reply *APIPostReply) error {
- post, err := api.api.CreatePost(args)
- *reply = APIPostReply{
- Post: post,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) DeletePost(args string, reply *APIErrorReply) error {
- *reply = APIErrorReply{
- Error: api.api.DeletePost(args),
- }
- return nil
-}
-
-func (api *LocalAPI) GetPost(args string, reply *APIPostReply) error {
- post, err := api.api.GetPost(args)
- *reply = APIPostReply{
- Post: post,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) UpdatePost(args *model.Post, reply *APIPostReply) error {
- post, err := api.api.UpdatePost(args)
- *reply = APIPostReply{
- Post: post,
- Error: err,
- }
- return nil
-}
-
-type APIKeyValueStoreReply struct {
- Value []byte
- Error *model.AppError
-}
-
-type APIKeyValueStoreSetArgs struct {
- Key string
- Value []byte
-}
-
-func (api *LocalAPI) KeyValueStoreSet(args *APIKeyValueStoreSetArgs, reply *APIErrorReply) error {
- err := api.api.KeyValueStore().Set(args.Key, args.Value)
- *reply = APIErrorReply{
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) KeyValueStoreGet(args string, reply *APIKeyValueStoreReply) error {
- v, err := api.api.KeyValueStore().Get(args)
- *reply = APIKeyValueStoreReply{
- Value: v,
- Error: err,
- }
- return nil
-}
-
-func (api *LocalAPI) KeyValueStoreDelete(args string, reply *APIErrorReply) error {
- err := api.api.KeyValueStore().Delete(args)
- *reply = APIErrorReply{
- Error: err,
- }
- return nil
-}
-
-func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) {
- server := rpc.NewServer()
- server.Register(&LocalAPI{
- api: api,
- muxer: muxer,
- })
- server.ServeConn(conn)
-}
-
-type RemoteAPI struct {
- client *rpc.Client
- muxer *Muxer
- keyValueStore *RemoteKeyValueStore
-}
-
-type RemoteKeyValueStore struct {
- api *RemoteAPI
-}
-
-var _ plugin.API = (*RemoteAPI)(nil)
-var _ plugin.KeyValueStore = (*RemoteKeyValueStore)(nil)
-
-func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
- var config []byte
- if err := api.client.Call("LocalAPI.LoadPluginConfiguration", struct{}{}, &config); err != nil {
- return err
- }
- return json.Unmarshal(config, dest)
-}
-
-func (api *RemoteAPI) RegisterCommand(command *model.Command) error {
- return api.client.Call("LocalAPI.RegisterCommand", command, nil)
-}
-
-type APIUnregisterCommandArgs struct {
- TeamId string
- Trigger string
-}
-
-func (api *RemoteAPI) UnregisterCommand(teamId, trigger string) error {
- return api.client.Call("LocalAPI.UnregisterCommand", &APIUnregisterCommandArgs{
- TeamId: teamId,
- Trigger: trigger,
- }, nil)
-}
-
-func (api *RemoteAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
- var reply APIUserReply
- if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.CreateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.User, reply.Error
-}
-
-func (api *RemoteAPI) DeleteUser(userId string) *model.AppError {
- var reply APIErrorReply
- if err := api.client.Call("LocalAPI.DeleteUser", userId, &reply); err != nil {
- return model.NewAppError("RemoteAPI.DeleteUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (api *RemoteAPI) GetUser(userId string) (*model.User, *model.AppError) {
- var reply APIUserReply
- if err := api.client.Call("LocalAPI.GetUser", userId, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.User, reply.Error
-}
-
-func (api *RemoteAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
- var reply APIUserReply
- if err := api.client.Call("LocalAPI.GetUserByEmail", email, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetUserByEmail", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.User, reply.Error
-}
-
-func (api *RemoteAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
- var reply APIUserReply
- if err := api.client.Call("LocalAPI.GetUserByUsername", name, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetUserByUsername", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.User, reply.Error
-}
-
-func (api *RemoteAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
- var reply APIUserReply
- if err := api.client.Call("LocalAPI.UpdateUser", user, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.User, reply.Error
-}
-
-func (api *RemoteAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
- var reply APITeamReply
- if err := api.client.Call("LocalAPI.CreateTeam", team, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.CreateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Team, reply.Error
-}
-
-func (api *RemoteAPI) DeleteTeam(teamId string) *model.AppError {
- var reply APIErrorReply
- if err := api.client.Call("LocalAPI.DeleteTeam", teamId, &reply); err != nil {
- return model.NewAppError("RemoteAPI.DeleteTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (api *RemoteAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
- var reply APITeamReply
- if err := api.client.Call("LocalAPI.GetTeam", teamId, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Team, reply.Error
-}
-
-func (api *RemoteAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
- var reply APITeamReply
- if err := api.client.Call("LocalAPI.GetTeamByName", name, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetTeamByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Team, reply.Error
-}
-
-func (api *RemoteAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
- var reply APITeamReply
- if err := api.client.Call("LocalAPI.UpdateTeam", team, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Team, reply.Error
-}
-
-func (api *RemoteAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.CreateChannel", channel, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.CreateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) DeleteChannel(channelId string) *model.AppError {
- var reply APIErrorReply
- if err := api.client.Call("LocalAPI.DeleteChannel", channelId, &reply); err != nil {
- return model.NewAppError("RemoteAPI.DeleteChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (api *RemoteAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.GetChannel", channelId, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.GetChannelByName", &APIGetChannelByNameArgs{
- Name: name,
- TeamId: teamId,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetChannelByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.GetDirectChannel", &APIGetDirectChannelArgs{
- UserId1: userId1,
- UserId2: userId2,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetDirectChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.GetGroupChannel", &APIGetGroupChannelArgs{
- UserIds: userIds,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetGroupChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
- var reply APIChannelReply
- if err := api.client.Call("LocalAPI.UpdateChannel", channel, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Channel, reply.Error
-}
-
-func (api *RemoteAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
- var reply APIChannelMemberReply
- if err := api.client.Call("LocalAPI.AddChannelMember", &APIAddChannelMemberArgs{
- ChannelId: channelId,
- UserId: userId,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.AddChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.ChannelMember, reply.Error
-}
-
-func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
- var reply APIChannelMemberReply
- if err := api.client.Call("LocalAPI.GetChannelMember", &APIGetChannelMemberArgs{
- ChannelId: channelId,
- UserId: userId,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.ChannelMember, reply.Error
-}
-
-func (api *RemoteAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
- var reply APIChannelMemberReply
- if err := api.client.Call("LocalAPI.UpdateChannelMemberRoles", &APIUpdateChannelMemberRolesArgs{
- ChannelId: channelId,
- UserId: userId,
- NewRoles: newRoles,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberRoles", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.ChannelMember, reply.Error
-}
-
-func (api *RemoteAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
- var reply APIChannelMemberReply
- if err := api.client.Call("LocalAPI.UpdateChannelMemberNotifications", &APIUpdateChannelMemberNotificationsArgs{
- ChannelId: channelId,
- UserId: userId,
- Notifications: notifications,
- }, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberNotifications", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.ChannelMember, reply.Error
-}
-
-func (api *RemoteAPI) DeleteChannelMember(channelId, userId string) *model.AppError {
- var reply APIErrorReply
- if err := api.client.Call("LocalAPI.DeleteChannelMember", &APIDeleteChannelMemberArgs{
- ChannelId: channelId,
- UserId: userId,
- }, &reply); err != nil {
- return model.NewAppError("RemoteAPI.DeleteChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (api *RemoteAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
- var reply APIPostReply
- if err := api.client.Call("LocalAPI.CreatePost", post, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.CreatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Post, reply.Error
-}
-
-func (api *RemoteAPI) DeletePost(postId string) *model.AppError {
- var reply APIErrorReply
- if err := api.client.Call("LocalAPI.DeletePost", postId, &reply); err != nil {
- return model.NewAppError("RemoteAPI.DeletePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (api *RemoteAPI) GetPost(postId string) (*model.Post, *model.AppError) {
- var reply APIPostReply
- if err := api.client.Call("LocalAPI.GetPost", postId, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.GetPost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Post, reply.Error
-}
-
-func (api *RemoteAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
- var reply APIPostReply
- if err := api.client.Call("LocalAPI.UpdatePost", post, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.UpdatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Post, reply.Error
-}
-
-func (api *RemoteAPI) KeyValueStore() plugin.KeyValueStore {
- return api.keyValueStore
-}
-
-func (s *RemoteKeyValueStore) Set(key string, value []byte) *model.AppError {
- var reply APIErrorReply
- if err := s.api.client.Call("LocalAPI.KeyValueStoreSet", &APIKeyValueStoreSetArgs{Key: key, Value: value}, &reply); err != nil {
- return model.NewAppError("RemoteAPI.KeyValueStoreSet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (s *RemoteKeyValueStore) Get(key string) ([]byte, *model.AppError) {
- var reply APIKeyValueStoreReply
- if err := s.api.client.Call("LocalAPI.KeyValueStoreGet", key, &reply); err != nil {
- return nil, model.NewAppError("RemoteAPI.KeyValueStoreGet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Value, reply.Error
-}
-
-func (s *RemoteKeyValueStore) Delete(key string) *model.AppError {
- var reply APIErrorReply
- if err := s.api.client.Call("LocalAPI.KeyValueStoreDelete", key, &reply); err != nil {
- return model.NewAppError("RemoteAPI.KeyValueStoreDelete", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Error
-}
-
-func (h *RemoteAPI) Close() error {
- return h.client.Close()
-}
-
-func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI {
- remoteKeyValueStore := &RemoteKeyValueStore{}
- remoteApi := &RemoteAPI{
- client: rpc.NewClient(conn),
- muxer: muxer,
- keyValueStore: remoteKeyValueStore,
- }
-
- remoteKeyValueStore.api = remoteApi
-
- return remoteApi
-}
-
-func init() {
- gob.Register([]*model.SlackAttachment{})
- gob.Register([]interface{}{})
- gob.Register(map[string]interface{}{})
-}
diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go
deleted file mode 100644
index 04d8e5d86..000000000
--- a/plugin/rpcplugin/api_test.go
+++ /dev/null
@@ -1,300 +0,0 @@
-package rpcplugin
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
-)
-
-func testAPIRPC(api plugin.API, f func(plugin.API)) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer c1.Close()
-
- c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer c2.Close()
-
- id, server := c1.Serve()
- go ServeAPI(api, server, c1)
-
- remote := ConnectAPI(c2.Connect(id), c2)
- defer remote.Close()
-
- f(remote)
-}
-
-func TestAPI(t *testing.T) {
- keyValueStore := &plugintest.KeyValueStore{}
- api := plugintest.API{Store: keyValueStore}
- defer api.AssertExpectations(t)
-
- type Config struct {
- Foo string
- Bar struct {
- Baz string
- }
- }
-
- api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Run(func(args mock.Arguments) {
- dest := args.Get(0).(interface{})
- json.Unmarshal([]byte(`{"Foo": "foo", "Bar": {"Baz": "baz"}}`), dest)
- }).Return(nil)
-
- testChannel := &model.Channel{
- Id: "thechannelid",
- }
-
- testChannelMember := &model.ChannelMember{
- ChannelId: "thechannelid",
- UserId: "theuserid",
- }
-
- testTeam := &model.Team{
- Id: "theteamid",
- }
- teamNotFoundError := model.NewAppError("SqlTeamStore.GetByName", "store.sql_team.get_by_name.app_error", nil, "name=notateam", http.StatusNotFound)
-
- testUser := &model.User{
- Id: "theuserid",
- }
-
- testPost := &model.Post{
- Message: "hello",
- }
-
- testAPIRPC(&api, func(remote plugin.API) {
- var config Config
- assert.NoError(t, remote.LoadPluginConfiguration(&config))
- assert.Equal(t, "foo", config.Foo)
- assert.Equal(t, "baz", config.Bar.Baz)
-
- api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(fmt.Errorf("foo")).Once()
- assert.Error(t, remote.RegisterCommand(&model.Command{}))
- api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(nil).Once()
- assert.NoError(t, remote.RegisterCommand(&model.Command{}))
-
- api.On("UnregisterCommand", "team", "trigger").Return(fmt.Errorf("foo")).Once()
- assert.Error(t, remote.UnregisterCommand("team", "trigger"))
- api.On("UnregisterCommand", "team", "trigger").Return(nil).Once()
- assert.NoError(t, remote.UnregisterCommand("team", "trigger"))
-
- api.On("CreateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel {
- c.Id = "thechannelid"
- return c
- }, nil).Once()
- channel, err := remote.CreateChannel(testChannel)
- assert.Equal(t, "thechannelid", channel.Id)
- assert.Nil(t, err)
-
- api.On("DeleteChannel", "thechannelid").Return(nil).Once()
- assert.Nil(t, remote.DeleteChannel("thechannelid"))
-
- api.On("GetChannel", "thechannelid").Return(testChannel, nil).Once()
- channel, err = remote.GetChannel("thechannelid")
- assert.Equal(t, testChannel, channel)
- assert.Nil(t, err)
-
- api.On("GetChannelByName", "foo", "theteamid").Return(testChannel, nil).Once()
- channel, err = remote.GetChannelByName("foo", "theteamid")
- assert.Equal(t, testChannel, channel)
- assert.Nil(t, err)
-
- api.On("GetDirectChannel", "user1", "user2").Return(testChannel, nil).Once()
- channel, err = remote.GetDirectChannel("user1", "user2")
- assert.Equal(t, testChannel, channel)
- assert.Nil(t, err)
-
- api.On("GetGroupChannel", []string{"user1", "user2", "user3"}).Return(testChannel, nil).Once()
- channel, err = remote.GetGroupChannel([]string{"user1", "user2", "user3"})
- assert.Equal(t, testChannel, channel)
- assert.Nil(t, err)
-
- api.On("UpdateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel {
- return c
- }, nil).Once()
- channel, err = remote.UpdateChannel(testChannel)
- assert.Equal(t, testChannel, channel)
- assert.Nil(t, err)
-
- api.On("AddChannelMember", testChannel.Id, "theuserid").Return(testChannelMember, nil).Once()
- member, err := remote.AddChannelMember(testChannel.Id, "theuserid")
- assert.Equal(t, testChannelMember, member)
- assert.Nil(t, err)
-
- api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once()
- member, err = remote.GetChannelMember("thechannelid", "theuserid")
- assert.Equal(t, testChannelMember, member)
- assert.Nil(t, err)
-
- api.On("UpdateChannelMemberRoles", testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID).Return(testChannelMember, nil).Once()
- member, err = remote.UpdateChannelMemberRoles(testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID)
- assert.Equal(t, testChannelMember, member)
- assert.Nil(t, err)
-
- notifications := map[string]string{}
- notifications[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_MENTION
- api.On("UpdateChannelMemberNotifications", testChannel.Id, "theuserid", notifications).Return(testChannelMember, nil).Once()
- member, err = remote.UpdateChannelMemberNotifications(testChannel.Id, "theuserid", notifications)
- assert.Equal(t, testChannelMember, member)
- assert.Nil(t, err)
-
- api.On("DeleteChannelMember", "thechannelid", "theuserid").Return(nil).Once()
- err = remote.DeleteChannelMember("thechannelid", "theuserid")
- assert.Nil(t, err)
-
- api.On("CreateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User {
- u.Id = "theuserid"
- return u
- }, nil).Once()
- user, err := remote.CreateUser(testUser)
- assert.Equal(t, "theuserid", user.Id)
- assert.Nil(t, err)
-
- api.On("DeleteUser", "theuserid").Return(nil).Once()
- assert.Nil(t, remote.DeleteUser("theuserid"))
-
- api.On("GetUser", "theuserid").Return(testUser, nil).Once()
- user, err = remote.GetUser("theuserid")
- assert.Equal(t, testUser, user)
- assert.Nil(t, err)
-
- api.On("GetUserByEmail", "foo@foo").Return(testUser, nil).Once()
- user, err = remote.GetUserByEmail("foo@foo")
- assert.Equal(t, testUser, user)
- assert.Nil(t, err)
-
- api.On("GetUserByUsername", "foo").Return(testUser, nil).Once()
- user, err = remote.GetUserByUsername("foo")
- assert.Equal(t, testUser, user)
- assert.Nil(t, err)
-
- api.On("UpdateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User {
- return u
- }, nil).Once()
- user, err = remote.UpdateUser(testUser)
- assert.Equal(t, testUser, user)
- assert.Nil(t, err)
-
- api.On("CreateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team {
- t.Id = "theteamid"
- return t
- }, nil).Once()
- team, err := remote.CreateTeam(testTeam)
- assert.Equal(t, "theteamid", team.Id)
- assert.Nil(t, err)
-
- api.On("DeleteTeam", "theteamid").Return(nil).Once()
- assert.Nil(t, remote.DeleteTeam("theteamid"))
-
- api.On("GetTeam", "theteamid").Return(testTeam, nil).Once()
- team, err = remote.GetTeam("theteamid")
- assert.Equal(t, testTeam, team)
- assert.Nil(t, err)
-
- api.On("GetTeamByName", "foo").Return(testTeam, nil).Once()
- team, err = remote.GetTeamByName("foo")
- assert.Equal(t, testTeam, team)
- assert.Nil(t, err)
-
- api.On("GetTeamByName", "notateam").Return(nil, teamNotFoundError).Once()
- team, err = remote.GetTeamByName("notateam")
- assert.Nil(t, team)
- assert.Equal(t, teamNotFoundError, err)
-
- api.On("UpdateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team {
- return t
- }, nil).Once()
- team, err = remote.UpdateTeam(testTeam)
- assert.Equal(t, testTeam, team)
- assert.Nil(t, err)
-
- api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
- p.Id = "thepostid"
- return p
- }, nil).Once()
- post, err := remote.CreatePost(testPost)
- require.Nil(t, err)
- assert.NotEmpty(t, post.Id)
- assert.Equal(t, testPost.Message, post.Message)
-
- api.On("DeletePost", "thepostid").Return(nil).Once()
- assert.Nil(t, remote.DeletePost("thepostid"))
-
- api.On("GetPost", "thepostid").Return(testPost, nil).Once()
- post, err = remote.GetPost("thepostid")
- assert.Equal(t, testPost, post)
- assert.Nil(t, err)
-
- api.On("UpdatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
- return p
- }, nil).Once()
- post, err = remote.UpdatePost(testPost)
- assert.Equal(t, testPost, post)
- assert.Nil(t, err)
-
- api.KeyValueStore().(*plugintest.KeyValueStore).On("Set", "thekey", []byte("thevalue")).Return(nil).Once()
- err = remote.KeyValueStore().Set("thekey", []byte("thevalue"))
- assert.Nil(t, err)
-
- api.KeyValueStore().(*plugintest.KeyValueStore).On("Get", "thekey").Return(func(key string) []byte {
- return []byte("thevalue")
- }, nil).Once()
- ret, err := remote.KeyValueStore().Get("thekey")
- assert.Nil(t, err)
- assert.Equal(t, []byte("thevalue"), ret)
-
- api.KeyValueStore().(*plugintest.KeyValueStore).On("Delete", "thekey").Return(nil).Once()
- err = remote.KeyValueStore().Delete("thekey")
- assert.Nil(t, err)
- })
-}
-
-func TestAPI_GobRegistration(t *testing.T) {
- keyValueStore := &plugintest.KeyValueStore{}
- api := plugintest.API{Store: keyValueStore}
- defer api.AssertExpectations(t)
-
- testAPIRPC(&api, func(remote plugin.API) {
- api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
- p.Id = "thepostid"
- return p
- }, nil).Once()
- _, err := remote.CreatePost(&model.Post{
- Message: "hello",
- Props: map[string]interface{}{
- "attachments": []*model.SlackAttachment{
- &model.SlackAttachment{
- Actions: []*model.PostAction{
- &model.PostAction{
- Integration: &model.PostActionIntegration{
- Context: map[string]interface{}{
- "foo": "bar",
- "foos": []interface{}{"bar", "baz", 1, 2},
- "foo_map": map[string]interface{}{
- "1": "bar",
- "2": 2,
- },
- },
- },
- },
- },
- Timestamp: 1,
- },
- },
- },
- })
- require.Nil(t, err)
- })
-}
diff --git a/plugin/rpcplugin/hooks.go b/plugin/rpcplugin/hooks.go
deleted file mode 100644
index 6af98873a..000000000
--- a/plugin/rpcplugin/hooks.go
+++ /dev/null
@@ -1,398 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "bytes"
- "io"
- "io/ioutil"
- "net/http"
- "net/rpc"
- "reflect"
-
- "github.com/mattermost/mattermost-server/mlog"
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
-)
-
-type LocalHooks struct {
- hooks interface{}
- muxer *Muxer
- remoteAPI *RemoteAPI
-}
-
-// Implemented replies with the names of the hooks that are implemented.
-func (h *LocalHooks) Implemented(args struct{}, reply *[]string) error {
- ifaceType := reflect.TypeOf((*plugin.Hooks)(nil)).Elem()
- implType := reflect.TypeOf(h.hooks)
- selfType := reflect.TypeOf(h)
- var methods []string
- for i := 0; i < ifaceType.NumMethod(); i++ {
- method := ifaceType.Method(i)
- if m, ok := implType.MethodByName(method.Name); !ok {
- continue
- } else if m.Type.NumIn() != method.Type.NumIn()+1 {
- continue
- } else if m.Type.NumOut() != method.Type.NumOut() {
- continue
- } else {
- match := true
- for j := 0; j < method.Type.NumIn(); j++ {
- if m.Type.In(j+1) != method.Type.In(j) {
- match = false
- break
- }
- }
- for j := 0; j < method.Type.NumOut(); j++ {
- if m.Type.Out(j) != method.Type.Out(j) {
- match = false
- break
- }
- }
- if !match {
- continue
- }
- }
- if _, ok := selfType.MethodByName(method.Name); !ok {
- continue
- }
- methods = append(methods, method.Name)
- }
- *reply = methods
- return nil
-}
-
-func (h *LocalHooks) OnActivate(args int64, reply *struct{}) error {
- if h.remoteAPI != nil {
- h.remoteAPI.Close()
- h.remoteAPI = nil
- }
- if hook, ok := h.hooks.(interface {
- OnActivate(plugin.API) error
- }); ok {
- stream := h.muxer.Connect(args)
- h.remoteAPI = ConnectAPI(stream, h.muxer)
- return hook.OnActivate(h.remoteAPI)
- }
- return nil
-}
-
-func (h *LocalHooks) OnDeactivate(args, reply *struct{}) (err error) {
- if hook, ok := h.hooks.(interface {
- OnDeactivate() error
- }); ok {
- err = hook.OnDeactivate()
- }
- if h.remoteAPI != nil {
- h.remoteAPI.Close()
- h.remoteAPI = nil
- }
- return
-}
-
-func (h *LocalHooks) OnConfigurationChange(args, reply *struct{}) error {
- if hook, ok := h.hooks.(interface {
- OnConfigurationChange() error
- }); ok {
- return hook.OnConfigurationChange()
- }
- return nil
-}
-
-type ServeHTTPArgs struct {
- ResponseWriterStream int64
- Request *http.Request
- RequestBodyStream int64
-}
-
-func (h *LocalHooks) ServeHTTP(args ServeHTTPArgs, reply *struct{}) error {
- w := ConnectHTTPResponseWriter(h.muxer.Connect(args.ResponseWriterStream))
- defer w.Close()
-
- r := args.Request
- if args.RequestBodyStream != 0 {
- r.Body = ConnectIOReader(h.muxer.Connect(args.RequestBodyStream))
- } else {
- r.Body = ioutil.NopCloser(&bytes.Buffer{})
- }
- defer r.Body.Close()
-
- if hook, ok := h.hooks.(http.Handler); ok {
- hook.ServeHTTP(w, r)
- } else {
- http.NotFound(w, r)
- }
-
- return nil
-}
-
-type HooksExecuteCommandReply struct {
- Response *model.CommandResponse
- Error *model.AppError
-}
-
-func (h *LocalHooks) ExecuteCommand(args *model.CommandArgs, reply *HooksExecuteCommandReply) error {
- if hook, ok := h.hooks.(interface {
- ExecuteCommand(*model.CommandArgs) (*model.CommandResponse, *model.AppError)
- }); ok {
- reply.Response, reply.Error = hook.ExecuteCommand(args)
- }
- return nil
-}
-
-type MessageWillBeReply struct {
- Post *model.Post
- RejectionReason string
-}
-
-type MessageUpdatedArgs struct {
- NewPost *model.Post
- OldPost *model.Post
-}
-
-func (h *LocalHooks) MessageWillBePosted(args *model.Post, reply *MessageWillBeReply) error {
- if hook, ok := h.hooks.(interface {
- MessageWillBePosted(*model.Post) (*model.Post, string)
- }); ok {
- reply.Post, reply.RejectionReason = hook.MessageWillBePosted(args)
- }
- return nil
-}
-
-func (h *LocalHooks) MessageWillBeUpdated(args *MessageUpdatedArgs, reply *MessageWillBeReply) error {
- if hook, ok := h.hooks.(interface {
- MessageWillBeUpdated(*model.Post, *model.Post) (*model.Post, string)
- }); ok {
- reply.Post, reply.RejectionReason = hook.MessageWillBeUpdated(args.NewPost, args.OldPost)
- }
- return nil
-}
-
-func (h *LocalHooks) MessageHasBeenPosted(args *model.Post, reply *struct{}) error {
- if hook, ok := h.hooks.(interface {
- MessageHasBeenPosted(*model.Post)
- }); ok {
- hook.MessageHasBeenPosted(args)
- }
- return nil
-}
-
-func (h *LocalHooks) MessageHasBeenUpdated(args *MessageUpdatedArgs, reply *struct{}) error {
- if hook, ok := h.hooks.(interface {
- MessageHasBeenUpdated(*model.Post, *model.Post)
- }); ok {
- hook.MessageHasBeenUpdated(args.NewPost, args.OldPost)
- }
- return nil
-}
-
-func ServeHooks(hooks interface{}, conn io.ReadWriteCloser, muxer *Muxer) {
- server := rpc.NewServer()
- server.Register(&LocalHooks{
- hooks: hooks,
- muxer: muxer,
- })
- server.ServeConn(conn)
-}
-
-// These assignments are part of the wire protocol. You can add more, but should not change existing
-// assignments.
-const (
- remoteOnActivate = 0
- remoteOnDeactivate = 1
- remoteServeHTTP = 2
- remoteOnConfigurationChange = 3
- remoteExecuteCommand = 4
- remoteMessageWillBePosted = 5
- remoteMessageWillBeUpdated = 6
- remoteMessageHasBeenPosted = 7
- remoteMessageHasBeenUpdated = 8
- maxRemoteHookCount = iota
-)
-
-type RemoteHooks struct {
- client *rpc.Client
- muxer *Muxer
- apiCloser io.Closer
- implemented [maxRemoteHookCount]bool
- pluginId string
-}
-
-var _ plugin.Hooks = (*RemoteHooks)(nil)
-
-func (h *RemoteHooks) Implemented() (impl []string, err error) {
- err = h.client.Call("LocalHooks.Implemented", struct{}{}, &impl)
- return
-}
-
-func (h *RemoteHooks) OnActivate(api plugin.API) error {
- if h.apiCloser != nil {
- h.apiCloser.Close()
- h.apiCloser = nil
- }
- if !h.implemented[remoteOnActivate] {
- return nil
- }
- id, stream := h.muxer.Serve()
- h.apiCloser = stream
- go ServeAPI(api, stream, h.muxer)
- return h.client.Call("LocalHooks.OnActivate", id, nil)
-}
-
-func (h *RemoteHooks) OnDeactivate() error {
- if !h.implemented[remoteOnDeactivate] {
- return nil
- }
- return h.client.Call("LocalHooks.OnDeactivate", struct{}{}, nil)
-}
-
-func (h *RemoteHooks) OnConfigurationChange() error {
- if !h.implemented[remoteOnConfigurationChange] {
- return nil
- }
- return h.client.Call("LocalHooks.OnConfigurationChange", struct{}{}, nil)
-}
-
-func (h *RemoteHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if !h.implemented[remoteServeHTTP] {
- http.NotFound(w, r)
- return
- }
-
- responseWriterStream, stream := h.muxer.Serve()
- defer stream.Close()
- go ServeHTTPResponseWriter(w, stream)
-
- requestBodyStream := int64(0)
- if r.Body != nil {
- rid, rstream := h.muxer.Serve()
- defer rstream.Close()
- go ServeIOReader(r.Body, rstream)
- requestBodyStream = rid
- }
-
- forwardedRequest := &http.Request{
- Method: r.Method,
- URL: r.URL,
- Proto: r.Proto,
- ProtoMajor: r.ProtoMajor,
- ProtoMinor: r.ProtoMinor,
- Header: r.Header,
- Host: r.Host,
- RemoteAddr: r.RemoteAddr,
- RequestURI: r.RequestURI,
- }
-
- if err := h.client.Call("LocalHooks.ServeHTTP", ServeHTTPArgs{
- ResponseWriterStream: responseWriterStream,
- Request: forwardedRequest,
- RequestBodyStream: requestBodyStream,
- }, nil); err != nil {
- mlog.Error("Plugin failed to ServeHTTP", mlog.String("plugin_id", h.pluginId), mlog.Err(err))
- http.Error(w, "500 internal server error", http.StatusInternalServerError)
- }
-}
-
-func (h *RemoteHooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
- if !h.implemented[remoteExecuteCommand] {
- return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err=ExecuteCommand hook not implemented", http.StatusInternalServerError)
- }
- var reply HooksExecuteCommandReply
- if err := h.client.Call("LocalHooks.ExecuteCommand", args, &reply); err != nil {
- return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
- }
- return reply.Response, reply.Error
-}
-
-func (h *RemoteHooks) MessageWillBePosted(args *model.Post) (*model.Post, string) {
- if !h.implemented[remoteMessageWillBePosted] {
- return args, ""
- }
- var reply MessageWillBeReply
- if err := h.client.Call("LocalHooks.MessageWillBePosted", args, &reply); err != nil {
- return nil, ""
- }
- return reply.Post, reply.RejectionReason
-}
-
-func (h *RemoteHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) {
- if !h.implemented[remoteMessageWillBeUpdated] {
- return newPost, ""
- }
- var reply MessageWillBeReply
- args := &MessageUpdatedArgs{
- NewPost: newPost,
- OldPost: oldPost,
- }
- if err := h.client.Call("LocalHooks.MessageWillBeUpdated", args, &reply); err != nil {
- return nil, ""
- }
- return reply.Post, reply.RejectionReason
-}
-
-func (h *RemoteHooks) MessageHasBeenPosted(args *model.Post) {
- if !h.implemented[remoteMessageHasBeenPosted] {
- return
- }
- if err := h.client.Call("LocalHooks.MessageHasBeenPosted", args, nil); err != nil {
- return
- }
-}
-
-func (h *RemoteHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) {
- if !h.implemented[remoteMessageHasBeenUpdated] {
- return
- }
- args := &MessageUpdatedArgs{
- NewPost: newPost,
- OldPost: oldPost,
- }
- if err := h.client.Call("LocalHooks.MessageHasBeenUpdated", args, nil); err != nil {
- return
- }
-}
-
-func (h *RemoteHooks) Close() error {
- if h.apiCloser != nil {
- h.apiCloser.Close()
- h.apiCloser = nil
- }
- return h.client.Close()
-}
-
-func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer, pluginId string) (*RemoteHooks, error) {
- remote := &RemoteHooks{
- client: rpc.NewClient(conn),
- muxer: muxer,
- pluginId: pluginId,
- }
- implemented, err := remote.Implemented()
- if err != nil {
- remote.Close()
- return nil, err
- }
- for _, method := range implemented {
- switch method {
- case "OnActivate":
- remote.implemented[remoteOnActivate] = true
- case "OnDeactivate":
- remote.implemented[remoteOnDeactivate] = true
- case "OnConfigurationChange":
- remote.implemented[remoteOnConfigurationChange] = true
- case "ServeHTTP":
- remote.implemented[remoteServeHTTP] = true
- case "ExecuteCommand":
- remote.implemented[remoteExecuteCommand] = true
- case "MessageWillBePosted":
- remote.implemented[remoteMessageWillBePosted] = true
- case "MessageWillBeUpdated":
- remote.implemented[remoteMessageWillBeUpdated] = true
- case "MessageHasBeenPosted":
- remote.implemented[remoteMessageHasBeenPosted] = true
- case "MessageHasBeenUpdated":
- remote.implemented[remoteMessageHasBeenUpdated] = true
- }
- }
- return remote, nil
-}
diff --git a/plugin/rpcplugin/hooks_test.go b/plugin/rpcplugin/hooks_test.go
deleted file mode 100644
index a7bac982e..000000000
--- a/plugin/rpcplugin/hooks_test.go
+++ /dev/null
@@ -1,237 +0,0 @@
-package rpcplugin
-
-import (
- "io"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
-)
-
-func testHooksRPC(hooks interface{}, f func(*RemoteHooks)) error {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer c1.Close()
-
- c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer c2.Close()
-
- id, server := c1.Serve()
- go ServeHooks(hooks, server, c1)
-
- remote, err := ConnectHooks(c2.Connect(id), c2, "plugin_id")
- if err != nil {
- return err
- }
- defer remote.Close()
-
- f(remote)
- return nil
-}
-
-func TestHooks(t *testing.T) {
- var api plugintest.API
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
- hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil)
- assert.NoError(t, remote.OnActivate(&api))
-
- hooks.On("OnDeactivate").Return(nil)
- assert.NoError(t, remote.OnDeactivate())
-
- hooks.On("OnConfigurationChange").Return(nil)
- assert.NoError(t, remote.OnConfigurationChange())
-
- hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
- w := args.Get(0).(http.ResponseWriter)
- r := args.Get(1).(*http.Request)
- assert.Equal(t, "/foo", r.URL.Path)
- assert.Equal(t, "POST", r.Method)
- body, err := ioutil.ReadAll(r.Body)
- assert.NoError(t, err)
- assert.Equal(t, "asdf", string(body))
- assert.Equal(t, "header", r.Header.Get("Test-Header"))
- w.Write([]byte("bar"))
- })
-
- w := httptest.NewRecorder()
- r, err := http.NewRequest("POST", "/foo", strings.NewReader("asdf"))
- r.Header.Set("Test-Header", "header")
- assert.NoError(t, err)
- remote.ServeHTTP(w, r)
-
- resp := w.Result()
- defer resp.Body.Close()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
- body, err := ioutil.ReadAll(resp.Body)
- assert.NoError(t, err)
- assert.Equal(t, "bar", string(body))
-
- hooks.On("ExecuteCommand", &model.CommandArgs{
- Command: "/foo",
- }).Return(&model.CommandResponse{
- Text: "bar",
- }, nil)
- commandResponse, appErr := hooks.ExecuteCommand(&model.CommandArgs{
- Command: "/foo",
- })
- assert.Equal(t, "bar", commandResponse.Text)
- assert.Nil(t, appErr)
-
- hooks.On("MessageWillBePosted", mock.AnythingOfType("*model.Post")).Return(func(post *model.Post) *model.Post {
- post.Message += "_testing"
- return post
- }, "changemessage")
- post, changemessage := remote.MessageWillBePosted(&model.Post{Id: "1", Message: "base"})
- assert.Equal(t, "changemessage", changemessage)
- assert.Equal(t, "base_testing", post.Message)
- assert.Equal(t, "1", post.Id)
-
- hooks.On("MessageWillBeUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(func(newPost, oldPost *model.Post) *model.Post {
- newPost.Message += "_testing"
- return newPost
- }, "changemessage2")
- post2, changemessage2 := remote.MessageWillBeUpdated(&model.Post{Id: "2", Message: "base2"}, &model.Post{Id: "OLD", Message: "OLDMESSAGE"})
- assert.Equal(t, "changemessage2", changemessage2)
- assert.Equal(t, "base2_testing", post2.Message)
- assert.Equal(t, "2", post2.Id)
-
- hooks.On("MessageHasBeenPosted", mock.AnythingOfType("*model.Post")).Return(nil)
- remote.MessageHasBeenPosted(&model.Post{})
-
- hooks.On("MessageHasBeenUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(nil)
- remote.MessageHasBeenUpdated(&model.Post{}, &model.Post{})
- }))
-}
-
-func TestHooks_Concurrency(t *testing.T) {
- var hooks plugintest.Hooks
- defer hooks.AssertExpectations(t)
-
- assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
- ch := make(chan bool)
-
- hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
- r := args.Get(1).(*http.Request)
- if r.URL.Path == "/1" {
- <-ch
- } else {
- ch <- true
- }
- })
-
- rec := httptest.NewRecorder()
-
- wg := sync.WaitGroup{}
- wg.Add(2)
-
- go func() {
- req, err := http.NewRequest("GET", "/1", nil)
- require.NoError(t, err)
- remote.ServeHTTP(rec, req)
- wg.Done()
- }()
-
- go func() {
- req, err := http.NewRequest("GET", "/2", nil)
- require.NoError(t, err)
- remote.ServeHTTP(rec, req)
- wg.Done()
- }()
-
- wg.Wait()
- }))
-}
-
-type testHooks struct {
- mock.Mock
-}
-
-func (h *testHooks) OnActivate(api plugin.API) error {
- return h.Called(api).Error(0)
-}
-
-func TestHooks_PartiallyImplemented(t *testing.T) {
- var api plugintest.API
- var hooks testHooks
- defer hooks.AssertExpectations(t)
-
- assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
- implemented, err := remote.Implemented()
- assert.NoError(t, err)
- assert.Equal(t, []string{"OnActivate"}, implemented)
-
- hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil)
- assert.NoError(t, remote.OnActivate(&api))
-
- assert.NoError(t, remote.OnDeactivate())
- }))
-}
-
-type benchmarkHooks struct{}
-
-func (*benchmarkHooks) OnDeactivate() error { return nil }
-
-func (*benchmarkHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ioutil.ReadAll(r.Body)
- w.Header().Set("Foo-Header", "foo")
- http.Error(w, "foo", http.StatusBadRequest)
-}
-
-func BenchmarkHooks_OnDeactivate(b *testing.B) {
- var hooks benchmarkHooks
-
- if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
- b.ResetTimer()
- for n := 0; n < b.N; n++ {
- remote.OnDeactivate()
- }
- b.StopTimer()
- }); err != nil {
- b.Fatal(err.Error())
- }
-}
-
-func BenchmarkHooks_ServeHTTP(b *testing.B) {
- var hooks benchmarkHooks
-
- if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
- b.ResetTimer()
- for n := 0; n < b.N; n++ {
- w := httptest.NewRecorder()
- r, _ := http.NewRequest("POST", "/foo", strings.NewReader("12345678901234567890"))
- remote.ServeHTTP(w, r)
- }
- b.StopTimer()
- }); err != nil {
- b.Fatal(err.Error())
- }
-}
-
-func BenchmarkHooks_Unimplemented(b *testing.B) {
- var hooks testHooks
-
- if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
- b.ResetTimer()
- for n := 0; n < b.N; n++ {
- remote.OnDeactivate()
- }
- b.StopTimer()
- }); err != nil {
- b.Fatal(err.Error())
- }
-}
diff --git a/plugin/rpcplugin/http.go b/plugin/rpcplugin/http.go
deleted file mode 100644
index 72b1aa445..000000000
--- a/plugin/rpcplugin/http.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "io"
- "net/http"
- "net/rpc"
-)
-
-type LocalHTTPResponseWriter struct {
- w http.ResponseWriter
-}
-
-func (w *LocalHTTPResponseWriter) Header(args struct{}, reply *http.Header) error {
- *reply = w.w.Header()
- return nil
-}
-
-func (w *LocalHTTPResponseWriter) Write(args []byte, reply *struct{}) error {
- _, err := w.w.Write(args)
- return err
-}
-
-func (w *LocalHTTPResponseWriter) WriteHeader(args int, reply *struct{}) error {
- w.w.WriteHeader(args)
- return nil
-}
-
-func (w *LocalHTTPResponseWriter) SyncHeader(args http.Header, reply *struct{}) error {
- dest := w.w.Header()
- for k := range dest {
- if _, ok := args[k]; !ok {
- delete(dest, k)
- }
- }
- for k, v := range args {
- dest[k] = v
- }
- return nil
-}
-
-func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) {
- server := rpc.NewServer()
- server.Register(&LocalHTTPResponseWriter{
- w: w,
- })
- server.ServeConn(conn)
-}
-
-type RemoteHTTPResponseWriter struct {
- client *rpc.Client
- header http.Header
-}
-
-var _ http.ResponseWriter = (*RemoteHTTPResponseWriter)(nil)
-
-func (w *RemoteHTTPResponseWriter) Header() http.Header {
- if w.header == nil {
- w.client.Call("LocalHTTPResponseWriter.Header", struct{}{}, &w.header)
- }
- return w.header
-}
-
-func (w *RemoteHTTPResponseWriter) Write(b []byte) (int, error) {
- if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil {
- return 0, err
- }
- if err := w.client.Call("LocalHTTPResponseWriter.Write", b, nil); err != nil {
- return 0, err
- }
- return len(b), nil
-}
-
-func (w *RemoteHTTPResponseWriter) WriteHeader(statusCode int) {
- if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil {
- return
- }
- w.client.Call("LocalHTTPResponseWriter.WriteHeader", statusCode, nil)
-}
-
-func (h *RemoteHTTPResponseWriter) Close() error {
- return h.client.Close()
-}
-
-func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *RemoteHTTPResponseWriter {
- return &RemoteHTTPResponseWriter{
- client: rpc.NewClient(conn),
- }
-}
diff --git a/plugin/rpcplugin/http_test.go b/plugin/rpcplugin/http_test.go
deleted file mode 100644
index afaaf7756..000000000
--- a/plugin/rpcplugin/http_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package rpcplugin
-
-import (
- "io"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func testHTTPResponseWriterRPC(w http.ResponseWriter, f func(w http.ResponseWriter)) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer c1.Close()
-
- c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer c2.Close()
-
- id, server := c1.Serve()
- go ServeHTTPResponseWriter(w, server)
-
- remote := ConnectHTTPResponseWriter(c2.Connect(id))
- defer remote.Close()
-
- f(remote)
-}
-
-func TestHTTP(t *testing.T) {
- w := httptest.NewRecorder()
-
- testHTTPResponseWriterRPC(w, func(w http.ResponseWriter) {
- headers := w.Header()
- headers.Set("Test-Header-A", "a")
- headers.Set("Test-Header-B", "b")
- w.Header().Set("Test-Header-C", "c")
- w.WriteHeader(http.StatusPaymentRequired)
- n, err := w.Write([]byte("this is "))
- assert.Equal(t, 8, n)
- assert.NoError(t, err)
- n, err = w.Write([]byte("a test"))
- assert.Equal(t, 6, n)
- assert.NoError(t, err)
- })
-
- r := w.Result()
- defer r.Body.Close()
-
- assert.Equal(t, http.StatusPaymentRequired, r.StatusCode)
-
- body, err := ioutil.ReadAll(r.Body)
- assert.NoError(t, err)
- assert.EqualValues(t, "this is a test", body)
-
- assert.Equal(t, "a", r.Header.Get("Test-Header-A"))
- assert.Equal(t, "b", r.Header.Get("Test-Header-B"))
- assert.Equal(t, "c", r.Header.Get("Test-Header-C"))
-}
diff --git a/plugin/rpcplugin/ipc.go b/plugin/rpcplugin/ipc.go
deleted file mode 100644
index e8dd43c04..000000000
--- a/plugin/rpcplugin/ipc.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "io"
- "os"
-)
-
-// Returns a new IPC for the parent process and a set of files to pass on to the child.
-//
-// The returned files must be closed after the child process is started.
-func NewIPC() (io.ReadWriteCloser, []*os.File, error) {
- parentReader, childWriter, err := os.Pipe()
- if err != nil {
- return nil, nil, err
- }
- childReader, parentWriter, err := os.Pipe()
- if err != nil {
- parentReader.Close()
- childWriter.Close()
- return nil, nil, err
- }
- return NewReadWriteCloser(parentReader, parentWriter), []*os.File{childReader, childWriter}, nil
-}
-
-// Returns the IPC instance inherited by the process from its parent.
-func InheritedIPC(fd0, fd1 uintptr) (io.ReadWriteCloser, error) {
- return NewReadWriteCloser(os.NewFile(fd0, ""), os.NewFile(fd1, "")), nil
-}
diff --git a/plugin/rpcplugin/ipc_test.go b/plugin/rpcplugin/ipc_test.go
deleted file mode 100644
index 76699a11e..000000000
--- a/plugin/rpcplugin/ipc_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package rpcplugin
-
-import (
- "context"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestIPC(t *testing.T) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- pingpong := filepath.Join(dir, "pingpong.exe")
- rpcplugintest.CompileGo(t, `
- package main
-
- import (
- "log"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- func main() {
- ipc, err := rpcplugin.InheritedProcessIPC()
- if err != nil {
- log.Fatal("unable to get inherited ipc")
- }
- defer ipc.Close()
- _, err = ipc.Write([]byte("ping"))
- if err != nil {
- log.Fatal("unable to write to ipc")
- }
- b := make([]byte, 10)
- n, err := ipc.Read(b)
- if err != nil {
- log.Fatal("unable to read from ipc")
- }
- if n != 4 || string(b[:4]) != "pong" {
- log.Fatal("unexpected response")
- }
- }
- `, pingpong)
-
- p, ipc, err := NewProcess(context.Background(), pingpong)
- require.NoError(t, err)
- defer ipc.Close()
- b := make([]byte, 10)
- n, err := ipc.Read(b)
- require.NoError(t, err)
- assert.Equal(t, 4, n)
- assert.Equal(t, "ping", string(b[:4]))
- _, err = ipc.Write([]byte("pong"))
- require.NoError(t, err)
- require.NoError(t, p.Wait())
-}
diff --git a/plugin/rpcplugin/main.go b/plugin/rpcplugin/main.go
deleted file mode 100644
index efb880605..000000000
--- a/plugin/rpcplugin/main.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "bufio"
- "encoding/binary"
- "fmt"
- "log"
- "os"
-)
-
-// Makes a set of hooks available via RPC. This function never returns.
-func Main(hooks interface{}) {
- ipc, err := InheritedProcessIPC()
- if err != nil {
- log.Fatal(err.Error())
- }
- muxer := NewMuxer(ipc, true)
- id, conn := muxer.Serve()
- buf := make([]byte, 11)
- buf[0] = 0
- n := binary.PutVarint(buf[1:], id)
- if _, err := muxer.Write(buf[:1+n]); err != nil {
- log.Fatal(err.Error())
- }
- ServeHooks(hooks, conn, muxer)
- os.Exit(0)
-}
-
-// Returns the hooks being served by a call to Main.
-func ConnectMain(muxer *Muxer, pluginId string) (*RemoteHooks, error) {
- buf := make([]byte, 1)
- if _, err := muxer.Read(buf); err != nil {
- return nil, err
- } else if buf[0] != 0 {
- return nil, fmt.Errorf("unexpected control byte")
- }
- reader := bufio.NewReader(muxer)
- id, err := binary.ReadVarint(reader)
- if err != nil {
- return nil, err
- }
-
- return ConnectHooks(muxer.Connect(id), muxer, pluginId)
-}
diff --git a/plugin/rpcplugin/main_test.go b/plugin/rpcplugin/main_test.go
deleted file mode 100644
index 06423106c..000000000
--- a/plugin/rpcplugin/main_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package rpcplugin
-
-import (
- "context"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/mlog"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestMain(t *testing.T) {
- // Setup a global logger to catch tests logging outside of app context
- // The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
- mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
- EnableConsole: true,
- ConsoleJson: true,
- ConsoleLevel: "error",
- EnableFile: false,
- }))
-
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- plugin := filepath.Join(dir, "plugin.exe")
- rpcplugintest.CompileGo(t, `
- package main
-
- import (
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- type MyPlugin struct {}
-
- func main() {
- rpcplugin.Main(&MyPlugin{})
- }
- `, plugin)
-
- ctx, cancel := context.WithCancel(context.Background())
- p, ipc, err := NewProcess(ctx, plugin)
- require.NoError(t, err)
- defer p.Wait()
-
- muxer := NewMuxer(ipc, false)
- defer muxer.Close()
-
- defer cancel()
-
- var api plugintest.API
-
- hooks, err := ConnectMain(muxer, "plugin_id")
- require.NoError(t, err)
- assert.NoError(t, hooks.OnActivate(&api))
- assert.NoError(t, hooks.OnDeactivate())
-}
diff --git a/plugin/rpcplugin/muxer.go b/plugin/rpcplugin/muxer.go
deleted file mode 100644
index a7260c399..000000000
--- a/plugin/rpcplugin/muxer.go
+++ /dev/null
@@ -1,264 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "bufio"
- "bytes"
- "encoding/binary"
- "fmt"
- "io"
- "sync"
- "sync/atomic"
-)
-
-// Muxer allows multiple bidirectional streams to be transmitted over a single connection.
-//
-// Muxer is safe for use by multiple goroutines.
-//
-// Streams opened on the muxer must be periodically drained in order to reclaim read buffer memory.
-// In other words, readers must consume incoming data as it comes in.
-type Muxer struct {
- // writeMutex guards conn writes
- writeMutex sync.Mutex
- conn io.ReadWriteCloser
-
- // didCloseConn is a boolean (0 or 1) used from multiple goroutines via atomic operations
- didCloseConn int32
-
- // streamsMutex guards streams and nextId
- streamsMutex sync.Mutex
- nextId int64
- streams map[int64]*muxerStream
-
- stream0Reader *io.PipeReader
- stream0Writer *io.PipeWriter
- result chan error
-}
-
-// Creates a new Muxer.
-//
-// conn must be safe for simultaneous reads by one goroutine and writes by another.
-//
-// For two muxers communicating with each other via a connection, parity must be true for exactly
-// one of them.
-func NewMuxer(conn io.ReadWriteCloser, parity bool) *Muxer {
- s0r, s0w := io.Pipe()
- muxer := &Muxer{
- conn: conn,
- streams: make(map[int64]*muxerStream),
- result: make(chan error, 1),
- nextId: 1,
- stream0Reader: s0r,
- stream0Writer: s0w,
- }
- if parity {
- muxer.nextId = 2
- }
- go muxer.run()
- return muxer
-}
-
-// Opens a new stream with a unique id.
-//
-// Writes made to the stream before the other end calls Connect will be discarded.
-func (m *Muxer) Serve() (int64, io.ReadWriteCloser) {
- m.streamsMutex.Lock()
- id := m.nextId
- m.nextId += 2
- m.streamsMutex.Unlock()
- return id, m.Connect(id)
-}
-
-// Opens a remotely opened stream.
-func (m *Muxer) Connect(id int64) io.ReadWriteCloser {
- m.streamsMutex.Lock()
- defer m.streamsMutex.Unlock()
- mutex := &sync.Mutex{}
- stream := &muxerStream{
- id: id,
- muxer: m,
- mutex: mutex,
- readWake: sync.NewCond(mutex),
- }
- m.streams[id] = stream
- return stream
-}
-
-// Calling Read on the muxer directly performs a read on a dedicated, always-open channel.
-func (m *Muxer) Read(p []byte) (int, error) {
- return m.stream0Reader.Read(p)
-}
-
-// Calling Write on the muxer directly performs a write on a dedicated, always-open channel.
-func (m *Muxer) Write(p []byte) (int, error) {
- return m.write(p, 0)
-}
-
-// Closes the muxer.
-func (m *Muxer) Close() error {
- if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) {
- m.conn.Close()
- }
- m.stream0Reader.Close()
- m.stream0Writer.Close()
- <-m.result
- return nil
-}
-
-func (m *Muxer) IsClosed() bool {
- return atomic.LoadInt32(&m.didCloseConn) > 0
-}
-
-func (m *Muxer) write(p []byte, sid int64) (int, error) {
- m.writeMutex.Lock()
- defer m.writeMutex.Unlock()
- if m.IsClosed() {
- return 0, fmt.Errorf("muxer closed")
- }
- var buf [10]byte
- n := binary.PutVarint(buf[:], sid)
- if _, err := m.conn.Write(buf[:n]); err != nil {
- m.shutdown(err)
- return 0, err
- }
- n = binary.PutVarint(buf[:], int64(len(p)))
- if _, err := m.conn.Write(buf[:n]); err != nil {
- m.shutdown(err)
- return 0, err
- }
- if len(p) > 0 {
- if _, err := m.conn.Write(p); err != nil {
- m.shutdown(err)
- return 0, err
- }
- }
- return len(p), nil
-}
-
-func (m *Muxer) rm(sid int64) {
- m.streamsMutex.Lock()
- defer m.streamsMutex.Unlock()
- delete(m.streams, sid)
-}
-
-func (m *Muxer) run() {
- m.shutdown(m.loop())
-}
-
-func (m *Muxer) loop() error {
- reader := bufio.NewReader(m.conn)
-
- for {
- sid, err := binary.ReadVarint(reader)
- if err != nil {
- return err
- }
- len, err := binary.ReadVarint(reader)
- if err != nil {
- return err
- }
-
- if sid == 0 {
- if _, err := io.CopyN(m.stream0Writer, reader, len); err != nil {
- return err
- }
- continue
- }
-
- m.streamsMutex.Lock()
- stream, ok := m.streams[sid]
- m.streamsMutex.Unlock()
- if !ok {
- if _, err := reader.Discard(int(len)); err != nil {
- return err
- }
- continue
- }
-
- stream.mutex.Lock()
- if stream.isClosed {
- stream.mutex.Unlock()
- if _, err := reader.Discard(int(len)); err != nil {
- return err
- }
- continue
- }
- if len == 0 {
- stream.remoteClosed = true
- } else {
- _, err = io.CopyN(&stream.readBuf, reader, len)
- }
- stream.mutex.Unlock()
- if err != nil {
- return err
- }
- stream.readWake.Signal()
- }
-}
-
-func (m *Muxer) shutdown(err error) {
- if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) {
- m.conn.Close()
- }
- go func() {
- m.streamsMutex.Lock()
- for _, stream := range m.streams {
- stream.mutex.Lock()
- stream.readWake.Signal()
- stream.mutex.Unlock()
- }
- m.streams = make(map[int64]*muxerStream)
- m.streamsMutex.Unlock()
- }()
- m.result <- err
-}
-
-type muxerStream struct {
- id int64
- muxer *Muxer
- readBuf bytes.Buffer
- mutex *sync.Mutex
- readWake *sync.Cond
- isClosed bool
- remoteClosed bool
-}
-
-func (s *muxerStream) Read(p []byte) (int, error) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- for {
- if s.muxer.IsClosed() {
- return 0, fmt.Errorf("muxer closed")
- } else if s.isClosed {
- return 0, io.EOF
- } else if s.readBuf.Len() > 0 {
- return s.readBuf.Read(p)
- } else if s.remoteClosed {
- return 0, io.EOF
- }
- s.readWake.Wait()
- }
-}
-
-func (s *muxerStream) Write(p []byte) (int, error) {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if s.isClosed {
- return 0, fmt.Errorf("stream closed")
- }
- return s.muxer.write(p, s.id)
-}
-
-func (s *muxerStream) Close() error {
- s.mutex.Lock()
- defer s.mutex.Unlock()
- if !s.isClosed {
- s.muxer.write(nil, s.id)
- s.isClosed = true
- s.muxer.rm(s.id)
- }
- s.readWake.Signal()
- return nil
-}
diff --git a/plugin/rpcplugin/muxer_test.go b/plugin/rpcplugin/muxer_test.go
deleted file mode 100644
index 795a4fb1d..000000000
--- a/plugin/rpcplugin/muxer_test.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package rpcplugin
-
-import (
- "io"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestMuxer(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer func() { assert.NoError(t, alice.Close()) }()
-
- bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer func() { assert.NoError(t, bob.Close()) }()
-
- id1, alice1 := alice.Serve()
- defer func() { assert.NoError(t, alice1.Close()) }()
-
- id2, bob2 := bob.Serve()
- defer func() { assert.NoError(t, bob2.Close()) }()
-
- done1 := make(chan bool)
- done2 := make(chan bool)
-
- go func() {
- bob1 := bob.Connect(id1)
- defer func() { assert.NoError(t, bob1.Close()) }()
-
- n, err := bob1.Write([]byte("ping1.0"))
- require.NoError(t, err)
- assert.Equal(t, n, 7)
-
- n, err = bob1.Write([]byte("ping1.1"))
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- }()
-
- go func() {
- alice2 := alice.Connect(id2)
- defer func() { assert.NoError(t, alice2.Close()) }()
-
- n, err := alice2.Write([]byte("ping2.0"))
- require.NoError(t, err)
- assert.Equal(t, n, 7)
-
- buf := make([]byte, 20)
- n, err = alice2.Read(buf)
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- assert.Equal(t, []byte("pong2.0"), buf[:n])
-
- done2 <- true
- }()
-
- go func() {
- buf := make([]byte, 7)
- n, err := io.ReadFull(alice1, buf)
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- assert.Equal(t, []byte("ping1.0"), buf[:n])
-
- n, err = alice1.Read(buf)
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- assert.Equal(t, []byte("ping1.1"), buf[:n])
-
- done1 <- true
- }()
-
- go func() {
- buf := make([]byte, 20)
- n, err := bob2.Read(buf)
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- assert.Equal(t, []byte("ping2.0"), buf[:n])
-
- n, err = bob2.Write([]byte("pong2.0"))
- require.NoError(t, err)
- assert.Equal(t, n, 7)
- }()
-
- <-done1
- <-done2
-}
-
-// Closing a muxer during a read should unblock, but return an error.
-func TestMuxer_CloseDuringRead(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
-
- bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer func() { assert.NoError(t, bob.Close()) }()
-
- _, s := alice.Serve()
-
- go alice.Close()
- buf := make([]byte, 20)
- n, err := s.Read(buf)
- assert.Equal(t, 0, n)
- assert.NotNil(t, err)
- assert.NotEqual(t, io.EOF, err)
-}
-
-// Closing a stream during a read should unblock and return io.EOF since this is the way to
-// gracefully close a connection.
-func TestMuxer_StreamCloseDuringRead(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer func() { assert.NoError(t, alice.Close()) }()
-
- bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer func() { assert.NoError(t, bob.Close()) }()
-
- _, s := alice.Serve()
-
- go s.Close()
- buf := make([]byte, 20)
- n, err := s.Read(buf)
- assert.Equal(t, 0, n)
- assert.Equal(t, io.EOF, err)
-}
-
-// Closing a stream during a read should unblock and return io.EOF since this is the way for the
-// remote to gracefully close a connection.
-func TestMuxer_RemoteStreamCloseDuringRead(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer func() { assert.NoError(t, alice.Close()) }()
-
- bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer func() { assert.NoError(t, bob.Close()) }()
-
- id, as := alice.Serve()
- bs := bob.Connect(id)
-
- go func() {
- as.Write([]byte("foo"))
- as.Close()
- }()
- buf := make([]byte, 20)
- n, err := bs.Read(buf)
- assert.Equal(t, 3, n)
- assert.Equal(t, "foo", string(buf[:n]))
- n, err = bs.Read(buf)
- assert.Equal(t, 0, n)
- assert.Equal(t, io.EOF, err)
-}
-
-// Closing a muxer during a write should unblock, but return an error.
-func TestMuxer_CloseDuringWrite(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
-
- // Don't connect bob to let writes will block forever.
- defer r2.Close()
- defer w1.Close()
-
- _, s := alice.Serve()
-
- go alice.Close()
- buf := make([]byte, 20)
- n, err := s.Write(buf)
- assert.Equal(t, 0, n)
- assert.NotNil(t, err)
- assert.NotEqual(t, io.EOF, err)
-}
-
-func TestMuxer_ReadWrite(t *testing.T) {
- r1, w1 := io.Pipe()
- r2, w2 := io.Pipe()
-
- alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
- defer func() { assert.NoError(t, alice.Close()) }()
-
- bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
- defer func() { assert.NoError(t, bob.Close()) }()
-
- go alice.Write([]byte("hello"))
- buf := make([]byte, 20)
- n, err := bob.Read(buf)
- assert.Equal(t, 5, n)
- assert.Nil(t, err)
- assert.Equal(t, []byte("hello"), buf[:n])
-}
diff --git a/plugin/rpcplugin/process.go b/plugin/rpcplugin/process.go
deleted file mode 100644
index a795be133..000000000
--- a/plugin/rpcplugin/process.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "context"
- "io"
-)
-
-type Process interface {
- // Waits for the process to exit and returns an error if a problem occurred or the process exited
- // with a non-zero status.
- Wait() error
-}
-
-// NewProcess launches an RPC executable in a new process and returns an IPC that can be used to
-// communicate with it.
-func NewProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
- return newProcess(ctx, path)
-}
-
-// When called on a process launched with NewProcess, returns the inherited IPC.
-func InheritedProcessIPC() (io.ReadWriteCloser, error) {
- return inheritedProcessIPC()
-}
diff --git a/plugin/rpcplugin/process_test.go b/plugin/rpcplugin/process_test.go
deleted file mode 100644
index 8d1794293..000000000
--- a/plugin/rpcplugin/process_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package rpcplugin
-
-import (
- "context"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestProcess(t *testing.T) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- ping := filepath.Join(dir, "ping.exe")
- rpcplugintest.CompileGo(t, `
- package main
-
- import (
- "log"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- func main() {
- ipc, err := rpcplugin.InheritedProcessIPC()
- if err != nil {
- log.Fatal("unable to get inherited ipc")
- }
- defer ipc.Close()
- _, err = ipc.Write([]byte("ping"))
- if err != nil {
- log.Fatal("unable to write to ipc")
- }
- }
- `, ping)
-
- p, ipc, err := NewProcess(context.Background(), ping)
- require.NoError(t, err)
- defer ipc.Close()
- b := make([]byte, 10)
- n, err := ipc.Read(b)
- require.NoError(t, err)
- assert.Equal(t, 4, n)
- assert.Equal(t, "ping", string(b[:4]))
- require.NoError(t, p.Wait())
-}
-
-func TestInvalidProcess(t *testing.T) {
- p, ipc, err := NewProcess(context.Background(), "thisfileshouldnotexist")
- require.Nil(t, p)
- require.Nil(t, ipc)
- require.Error(t, err)
-}
diff --git a/plugin/rpcplugin/process_unix.go b/plugin/rpcplugin/process_unix.go
deleted file mode 100644
index 142043cc6..000000000
--- a/plugin/rpcplugin/process_unix.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-// +build !windows
-
-package rpcplugin
-
-import (
- "context"
- "io"
- "os"
- "os/exec"
-)
-
-type process struct {
- command *exec.Cmd
-}
-
-func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
- ipc, childFiles, err := NewIPC()
- if err != nil {
- return nil, nil, err
- }
- defer childFiles[0].Close()
- defer childFiles[1].Close()
-
- cmd := exec.CommandContext(ctx, path)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.ExtraFiles = childFiles
- err = cmd.Start()
- if err != nil {
- ipc.Close()
- return nil, nil, err
- }
-
- return &process{
- command: cmd,
- }, ipc, nil
-}
-
-func (p *process) Wait() error {
- return p.command.Wait()
-}
-
-func inheritedProcessIPC() (io.ReadWriteCloser, error) {
- return InheritedIPC(3, 4)
-}
diff --git a/plugin/rpcplugin/process_windows.go b/plugin/rpcplugin/process_windows.go
deleted file mode 100644
index 069f147c1..000000000
--- a/plugin/rpcplugin/process_windows.go
+++ /dev/null
@@ -1,648 +0,0 @@
-package rpcplugin
-
-import (
- "context"
- "errors"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strconv"
- "strings"
- "syscall"
- "unicode/utf16"
- "unsafe"
-
- pkgerrors "github.com/pkg/errors"
-)
-
-type process struct {
- command *cmd
-}
-
-func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
- ipc, childFiles, err := NewIPC()
- if err != nil {
- return nil, nil, err
- }
- defer childFiles[0].Close()
- defer childFiles[1].Close()
-
- cmd := commandContext(ctx, path)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.ExtraFiles = childFiles
- cmd.Env = append(os.Environ(),
- fmt.Sprintf("MM_IPC_FD0=%v", childFiles[0].Fd()),
- fmt.Sprintf("MM_IPC_FD1=%v", childFiles[1].Fd()),
- )
- err = cmd.Start()
- if err != nil {
- ipc.Close()
- return nil, nil, err
- }
-
- return &process{
- command: cmd,
- }, ipc, nil
-}
-
-func (p *process) Wait() error {
- return p.command.Wait()
-}
-
-func inheritedProcessIPC() (io.ReadWriteCloser, error) {
- fd0, err := strconv.ParseUint(os.Getenv("MM_IPC_FD0"), 0, 64)
- if err != nil {
- return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 0")
- }
- fd1, err := strconv.ParseUint(os.Getenv("MM_IPC_FD1"), 0, 64)
- if err != nil {
- return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 1")
- }
- return InheritedIPC(uintptr(fd0), uintptr(fd1))
-}
-
-// XXX: EVERYTHING BELOW THIS IS COPIED / PASTED STANDARD LIBRARY CODE!
-// IT CAN BE DELETED IF / WHEN THIS ISSUE IS RESOLVED: https://github.com/golang/go/issues/21085
-
-// Just about all of os/exec/exec.go is copied / pasted below, altered to use our modified startProcess functions even
-// further below.
-
-type cmd struct {
- // Path is the path of the command to run.
- //
- // This is the only field that must be set to a non-zero
- // value. If Path is relative, it is evaluated relative
- // to Dir.
- Path string
-
- // Args holds command line arguments, including the command as Args[0].
- // If the Args field is empty or nil, Run uses {Path}.
- //
- // In typical use, both Path and Args are set by calling Command.
- Args []string
-
- // Env specifies the environment of the process.
- // If Env is nil, Run uses the current process's environment.
- Env []string
-
- // Dir specifies the working directory of the command.
- // If Dir is the empty string, Run runs the command in the
- // calling process's current directory.
- Dir string
-
- // Stdin specifies the process's standard input.
- // If Stdin is nil, the process reads from the null device (os.DevNull).
- // If Stdin is an *os.File, the process's standard input is connected
- // directly to that file.
- // Otherwise, during the execution of the command a separate
- // goroutine reads from Stdin and delivers that data to the command
- // over a pipe. In this case, Wait does not complete until the goroutine
- // stops copying, either because it has reached the end of Stdin
- // (EOF or a read error) or because writing to the pipe returned an error.
- Stdin io.Reader
-
- // Stdout and Stderr specify the process's standard output and error.
- //
- // If either is nil, Run connects the corresponding file descriptor
- // to the null device (os.DevNull).
- //
- // If Stdout and Stderr are the same writer, at most one
- // goroutine at a time will call Write.
- Stdout io.Writer
- Stderr io.Writer
-
- // ExtraFiles specifies additional open files to be inherited by the
- // new process. It does not include standard input, standard output, or
- // standard error. If non-nil, entry i becomes file descriptor 3+i.
- //
- // BUG(rsc): On OS X 10.6, child processes may sometimes inherit unwanted fds.
- // https://golang.org/issue/2603
- ExtraFiles []*os.File
-
- // SysProcAttr holds optional, operating system-specific attributes.
- // Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
- SysProcAttr *syscall.SysProcAttr
-
- // Process is the underlying process, once started.
- Process *os.Process
-
- // ProcessState contains information about an exited process,
- // available after a call to Wait or Run.
- ProcessState *os.ProcessState
-
- ctx context.Context // nil means none
- lookPathErr error // LookPath error, if any.
- finished bool // when Wait was called
- childFiles []*os.File
- closeAfterStart []io.Closer
- closeAfterWait []io.Closer
- goroutine []func() error
- errch chan error // one send per goroutine
- waitDone chan struct{}
-}
-
-func command(name string, arg ...string) *cmd {
- cmd := &cmd{
- Path: name,
- Args: append([]string{name}, arg...),
- }
- if filepath.Base(name) == name {
- if lp, err := exec.LookPath(name); err != nil {
- cmd.lookPathErr = err
- } else {
- cmd.Path = lp
- }
- }
- return cmd
-}
-
-func commandContext(ctx context.Context, name string, arg ...string) *cmd {
- if ctx == nil {
- panic("nil Context")
- }
- cmd := command(name, arg...)
- cmd.ctx = ctx
- return cmd
-}
-
-func interfaceEqual(a, b interface{}) bool {
- defer func() {
- recover()
- }()
- return a == b
-}
-
-func (c *cmd) envv() []string {
- if c.Env != nil {
- return c.Env
- }
- return os.Environ()
-}
-
-func (c *cmd) argv() []string {
- if len(c.Args) > 0 {
- return c.Args
- }
- return []string{c.Path}
-}
-
-var skipStdinCopyError func(error) bool
-
-func (c *cmd) stdin() (f *os.File, err error) {
- if c.Stdin == nil {
- f, err = os.Open(os.DevNull)
- if err != nil {
- return
- }
- c.closeAfterStart = append(c.closeAfterStart, f)
- return
- }
-
- if f, ok := c.Stdin.(*os.File); ok {
- return f, nil
- }
-
- pr, pw, err := os.Pipe()
- if err != nil {
- return
- }
-
- c.closeAfterStart = append(c.closeAfterStart, pr)
- c.closeAfterWait = append(c.closeAfterWait, pw)
- c.goroutine = append(c.goroutine, func() error {
- _, err := io.Copy(pw, c.Stdin)
- if skip := skipStdinCopyError; skip != nil && skip(err) {
- err = nil
- }
- if err1 := pw.Close(); err == nil {
- err = err1
- }
- return err
- })
- return pr, nil
-}
-
-func (c *cmd) stdout() (f *os.File, err error) {
- return c.writerDescriptor(c.Stdout)
-}
-
-func (c *cmd) stderr() (f *os.File, err error) {
- if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) {
- return c.childFiles[1], nil
- }
- return c.writerDescriptor(c.Stderr)
-}
-
-func (c *cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
- if w == nil {
- f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
- if err != nil {
- return
- }
- c.closeAfterStart = append(c.closeAfterStart, f)
- return
- }
-
- if f, ok := w.(*os.File); ok {
- return f, nil
- }
-
- pr, pw, err := os.Pipe()
- if err != nil {
- return
- }
-
- c.closeAfterStart = append(c.closeAfterStart, pw)
- c.closeAfterWait = append(c.closeAfterWait, pr)
- c.goroutine = append(c.goroutine, func() error {
- _, err := io.Copy(w, pr)
- pr.Close() // in case io.Copy stopped due to write error
- return err
- })
- return pw, nil
-}
-
-func (c *cmd) closeDescriptors(closers []io.Closer) {
- for _, fd := range closers {
- fd.Close()
- }
-}
-
-func lookExtensions(path, dir string) (string, error) {
- if filepath.Base(path) == path {
- path = filepath.Join(".", path)
- }
- if dir == "" {
- return exec.LookPath(path)
- }
- if filepath.VolumeName(path) != "" {
- return exec.LookPath(path)
- }
- if len(path) > 1 && os.IsPathSeparator(path[0]) {
- return exec.LookPath(path)
- }
- dirandpath := filepath.Join(dir, path)
- // We assume that LookPath will only add file extension.
- lp, err := exec.LookPath(dirandpath)
- if err != nil {
- return "", err
- }
- ext := strings.TrimPrefix(lp, dirandpath)
- return path + ext, nil
-}
-
-// Copied from os/exec/exec.go, altered to use osStartProcess (defined below).
-func (c *cmd) Start() error {
- if c.lookPathErr != nil {
- c.closeDescriptors(c.closeAfterStart)
- c.closeDescriptors(c.closeAfterWait)
- return c.lookPathErr
- }
- if runtime.GOOS == "windows" {
- lp, err := lookExtensions(c.Path, c.Dir)
- if err != nil {
- c.closeDescriptors(c.closeAfterStart)
- c.closeDescriptors(c.closeAfterWait)
- return err
- }
- c.Path = lp
- }
- if c.Process != nil {
- return errors.New("exec: already started")
- }
- if c.ctx != nil {
- select {
- case <-c.ctx.Done():
- c.closeDescriptors(c.closeAfterStart)
- c.closeDescriptors(c.closeAfterWait)
- return c.ctx.Err()
- default:
- }
- }
-
- type F func(*cmd) (*os.File, error)
- for _, setupFd := range []F{(*cmd).stdin, (*cmd).stdout, (*cmd).stderr} {
- fd, err := setupFd(c)
- if err != nil {
- c.closeDescriptors(c.closeAfterStart)
- c.closeDescriptors(c.closeAfterWait)
- return err
- }
- c.childFiles = append(c.childFiles, fd)
- }
- c.childFiles = append(c.childFiles, c.ExtraFiles...)
-
- var err error
- c.Process, err = osStartProcess(c.Path, c.argv(), &os.ProcAttr{
- Dir: c.Dir,
- Files: c.childFiles,
- Env: c.envv(),
- Sys: c.SysProcAttr,
- })
- if err != nil {
- c.closeDescriptors(c.closeAfterStart)
- c.closeDescriptors(c.closeAfterWait)
- return err
- }
-
- c.closeDescriptors(c.closeAfterStart)
-
- c.errch = make(chan error, len(c.goroutine))
- for _, fn := range c.goroutine {
- go func(fn func() error) {
- c.errch <- fn()
- }(fn)
- }
-
- if c.ctx != nil {
- c.waitDone = make(chan struct{})
- go func() {
- select {
- case <-c.ctx.Done():
- c.Process.Kill()
- case <-c.waitDone:
- }
- }()
- }
-
- return nil
-}
-
-func (c *cmd) Wait() error {
- if c.Process == nil {
- return errors.New("exec: not started")
- }
- if c.finished {
- return errors.New("exec: Wait was already called")
- }
- c.finished = true
-
- state, err := c.Process.Wait()
- if c.waitDone != nil {
- close(c.waitDone)
- }
- c.ProcessState = state
-
- var copyError error
- for range c.goroutine {
- if err := <-c.errch; err != nil && copyError == nil {
- copyError = err
- }
- }
-
- c.closeDescriptors(c.closeAfterWait)
-
- if err != nil {
- return err
- } else if !state.Success() {
- return &exec.ExitError{ProcessState: state}
- }
-
- return copyError
-}
-
-// Copied from os/exec_posix.go, altered to use syscallStartProcess (defined below).
-func osStartProcess(name string, argv []string, attr *os.ProcAttr) (p *os.Process, err error) {
- // If there is no SysProcAttr (ie. no Chroot or changed
- // UID/GID), double-check existence of the directory we want
- // to chdir into. We can make the error clearer this way.
- if attr != nil && attr.Sys == nil && attr.Dir != "" {
- if _, err := os.Stat(attr.Dir); err != nil {
- pe := err.(*os.PathError)
- pe.Op = "chdir"
- return nil, pe
- }
- }
-
- sysattr := &syscall.ProcAttr{
- Dir: attr.Dir,
- Env: attr.Env,
- Sys: attr.Sys,
- }
- if sysattr.Env == nil {
- sysattr.Env = os.Environ()
- }
- for _, f := range attr.Files {
- sysattr.Files = append(sysattr.Files, f.Fd())
- }
-
- pid, _, e := syscallStartProcess(name, argv, sysattr)
- if e != nil {
- return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e}
- }
- return os.FindProcess(pid)
-}
-
-// Everything from this point on is copied from syscall/exec_windows.go
-
-func makeCmdLine(args []string) string {
- var s string
- for _, v := range args {
- if s != "" {
- s += " "
- }
- s += syscall.EscapeArg(v)
- }
- return s
-}
-
-func createEnvBlock(envv []string) *uint16 {
- if len(envv) == 0 {
- return &utf16.Encode([]rune("\x00\x00"))[0]
- }
- length := 0
- for _, s := range envv {
- length += len(s) + 1
- }
- length += 1
-
- b := make([]byte, length)
- i := 0
- for _, s := range envv {
- l := len(s)
- copy(b[i:i+l], []byte(s))
- copy(b[i+l:i+l+1], []byte{0})
- i = i + l + 1
- }
- copy(b[i:i+1], []byte{0})
-
- return &utf16.Encode([]rune(string(b)))[0]
-}
-
-func isSlash(c uint8) bool {
- return c == '\\' || c == '/'
-}
-
-func normalizeDir(dir string) (name string, err error) {
- ndir, err := syscall.FullPath(dir)
- if err != nil {
- return "", err
- }
- if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
- // dir cannot have \\server\share\path form
- return "", syscall.EINVAL
- }
- return ndir, nil
-}
-
-func volToUpper(ch int) int {
- if 'a' <= ch && ch <= 'z' {
- ch += 'A' - 'a'
- }
- return ch
-}
-
-func joinExeDirAndFName(dir, p string) (name string, err error) {
- if len(p) == 0 {
- return "", syscall.EINVAL
- }
- if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
- // \\server\share\path form
- return p, nil
- }
- if len(p) > 1 && p[1] == ':' {
- // has drive letter
- if len(p) == 2 {
- return "", syscall.EINVAL
- }
- if isSlash(p[2]) {
- return p, nil
- } else {
- d, err := normalizeDir(dir)
- if err != nil {
- return "", err
- }
- if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
- return syscall.FullPath(d + "\\" + p[2:])
- } else {
- return syscall.FullPath(p)
- }
- }
- } else {
- // no drive letter
- d, err := normalizeDir(dir)
- if err != nil {
- return "", err
- }
- if isSlash(p[0]) {
- return syscall.FullPath(d[:2] + p)
- } else {
- return syscall.FullPath(d + "\\" + p)
- }
- }
-}
-
-var zeroProcAttr syscall.ProcAttr
-var zeroSysProcAttr syscall.SysProcAttr
-
-// Has minor changes to support file inheritance.
-func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) {
- if len(argv0) == 0 {
- return 0, 0, syscall.EWINDOWS
- }
- if attr == nil {
- attr = &zeroProcAttr
- }
- sys := attr.Sys
- if sys == nil {
- sys = &zeroSysProcAttr
- }
-
- if len(attr.Files) < 3 {
- return 0, 0, syscall.EINVAL
- }
-
- if len(attr.Dir) != 0 {
- // StartProcess assumes that argv0 is relative to attr.Dir,
- // because it implies Chdir(attr.Dir) before executing argv0.
- // Windows CreateProcess assumes the opposite: it looks for
- // argv0 relative to the current directory, and, only once the new
- // process is started, it does Chdir(attr.Dir). We are adjusting
- // for that difference here by making argv0 absolute.
- var err error
- argv0, err = joinExeDirAndFName(attr.Dir, argv0)
- if err != nil {
- return 0, 0, err
- }
- }
- argv0p, err := syscall.UTF16PtrFromString(argv0)
- if err != nil {
- return 0, 0, err
- }
-
- var cmdline string
- // Windows CreateProcess takes the command line as a single string:
- // use attr.CmdLine if set, else build the command line by escaping
- // and joining each argument with spaces
- if sys.CmdLine != "" {
- cmdline = sys.CmdLine
- } else {
- cmdline = makeCmdLine(argv)
- }
-
- var argvp *uint16
- if len(cmdline) != 0 {
- argvp, err = syscall.UTF16PtrFromString(cmdline)
- if err != nil {
- return 0, 0, err
- }
- }
-
- var dirp *uint16
- if len(attr.Dir) != 0 {
- dirp, err = syscall.UTF16PtrFromString(attr.Dir)
- if err != nil {
- return 0, 0, err
- }
- }
-
- // Acquire the fork lock so that no other threads
- // create new fds that are not yet close-on-exec
- // before we fork.
- syscall.ForkLock.Lock()
- defer syscall.ForkLock.Unlock()
-
- p, _ := syscall.GetCurrentProcess()
- fd := make([]syscall.Handle, len(attr.Files))
- for i := range attr.Files {
- if attr.Files[i] <= 0 {
- continue
- }
- if i < 3 {
- err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
- if err != nil {
- return 0, 0, err
- }
- defer syscall.CloseHandle(syscall.Handle(fd[i]))
- } else {
- // This is the modification that allows files to be inherited.
- syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 1)
- defer syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 0)
- }
- }
- si := new(syscall.StartupInfo)
- si.Cb = uint32(unsafe.Sizeof(*si))
- si.Flags = syscall.STARTF_USESTDHANDLES
- if sys.HideWindow {
- si.Flags |= syscall.STARTF_USESHOWWINDOW
- si.ShowWindow = syscall.SW_HIDE
- }
- si.StdInput = fd[0]
- si.StdOutput = fd[1]
- si.StdErr = fd[2]
-
- pi := new(syscall.ProcessInformation)
-
- flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
- err = syscall.CreateProcess(argv0p, argvp, nil, nil, true, flags, createEnvBlock(attr.Env), dirp, si, pi)
- if err != nil {
- return 0, 0, err
- }
- defer syscall.CloseHandle(syscall.Handle(pi.Thread))
-
- return int(pi.ProcessId), uintptr(pi.Process), nil
-}
diff --git a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go b/plugin/rpcplugin/rpcplugintest/rpcplugintest.go
deleted file mode 100644
index 185f741c1..000000000
--- a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugintest
-
-import (
- "io/ioutil"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func CompileGo(t *testing.T, sourceCode, outputPath string) {
- dir, err := ioutil.TempDir(".", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
- require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600))
- cmd := exec.Command("go", "build", "-o", outputPath, "main.go")
- cmd.Dir = dir
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- require.NoError(t, cmd.Run())
-}
diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go
deleted file mode 100644
index f3ff847a2..000000000
--- a/plugin/rpcplugin/rpcplugintest/supervisor.go
+++ /dev/null
@@ -1,312 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugintest
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/plugintest"
-)
-
-type SupervisorProviderFunc = func(*model.BundleInfo) (plugin.Supervisor, error)
-
-func TestSupervisorProvider(t *testing.T, sp SupervisorProviderFunc) {
- for name, f := range map[string]func(*testing.T, SupervisorProviderFunc){
- "Supervisor": testSupervisor,
- "Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath,
- "Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath,
- "Supervisor_StartTimeout": testSupervisor_StartTimeout,
- // "Supervisor_PluginCrash": testSupervisor_PluginCrash,
- // "Supervisor_PluginRepeatedlyCrash": testSupervisor_PluginRepeatedlyCrash,
- } {
- t.Run(name, func(t *testing.T) { f(t, sp) })
- }
-}
-
-func testSupervisor(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- backend := filepath.Join(dir, "backend.exe")
- CompileGo(t, `
- package main
-
- import (
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- type MyPlugin struct {}
-
- func main() {
- rpcplugin.Main(&MyPlugin{})
- }
- `, backend)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
-
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- require.NoError(t, err)
- require.NoError(t, supervisor.Start(nil))
- require.NoError(t, supervisor.Stop())
-}
-
-func testSupervisor_InvalidExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600)
-
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- assert.Nil(t, supervisor)
- assert.Error(t, err)
-}
-
-func testSupervisor_NonExistentExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600)
-
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- require.NotNil(t, supervisor)
- require.NoError(t, err)
-
- require.Error(t, supervisor.Start(nil))
-}
-
-// If plugin development goes really wrong, let's make sure plugin activation won't block forever.
-func testSupervisor_StartTimeout(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- backend := filepath.Join(dir, "backend.exe")
- CompileGo(t, `
- package main
-
- func main() {
- for {
- }
- }
- `, backend)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
-
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- require.NoError(t, err)
- require.Error(t, supervisor.Start(nil))
-}
-
-// Crashed plugins should be relaunched.
-func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- backend := filepath.Join(dir, "backend.exe")
- CompileGo(t, `
- package main
-
- import (
- "os"
-
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- type Configuration struct {
- ShouldExit bool
- }
-
- type MyPlugin struct {
- config Configuration
- }
-
- func (p *MyPlugin) OnActivate(api plugin.API) error {
- api.LoadPluginConfiguration(&p.config)
- return nil
- }
-
- func (p *MyPlugin) OnDeactivate() error {
- if p.config.ShouldExit {
- os.Exit(1)
- }
- return nil
- }
-
- func main() {
- rpcplugin.Main(&MyPlugin{})
- }
- `, backend)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
-
- var api plugintest.API
- shouldExit := true
- api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error {
- err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest)
- shouldExit = false
- return err
- })
-
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- require.NoError(t, err)
-
- var supervisorWaitErr error
- supervisorWaitDone := make(chan bool, 1)
- go func() {
- supervisorWaitErr = supervisor.Wait()
- close(supervisorWaitDone)
- }()
-
- require.NoError(t, supervisor.Start(&api))
-
- failed := false
- recovered := false
- for i := 0; i < 30; i++ {
- if supervisor.Hooks().OnDeactivate() == nil {
- require.True(t, failed)
- recovered = true
- break
- } else {
- failed = true
- }
- time.Sleep(time.Millisecond * 100)
- }
- assert.True(t, recovered)
-
- select {
- case <-supervisorWaitDone:
- require.Fail(t, "supervisor.Wait() unexpectedly returned")
- case <-time.After(500 * time.Millisecond):
- }
-
- require.NoError(t, supervisor.Stop())
-
- select {
- case <-supervisorWaitDone:
- require.Nil(t, supervisorWaitErr)
- case <-time.After(5000 * time.Millisecond):
- require.Fail(t, "supervisor.Wait() failed to return")
- }
-}
-
-// Crashed plugins should be relaunched at most three times.
-func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFunc) {
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- backend := filepath.Join(dir, "backend.exe")
- CompileGo(t, `
- package main
-
- import (
- "net/http"
- "os"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- type MyPlugin struct {
- crashing bool
- }
-
- func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- p.crashing = true
- go func() {
- os.Exit(1)
- }()
- }
-
- if p.crashing {
- w.WriteHeader(http.StatusInternalServerError)
- } else {
- w.WriteHeader(http.StatusOK)
- }
- }
-
- func main() {
- rpcplugin.Main(&MyPlugin{})
- }
- `, backend)
-
- ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
-
- var api plugintest.API
- bundle := model.BundleInfoForPath(dir)
- supervisor, err := sp(bundle)
- require.NoError(t, err)
-
- var supervisorWaitErr error
- supervisorWaitDone := make(chan bool, 1)
- go func() {
- supervisorWaitErr = supervisor.Wait()
- close(supervisorWaitDone)
- }()
-
- require.NoError(t, supervisor.Start(&api))
-
- for attempt := 1; attempt <= 4; attempt++ {
- // Verify that the plugin is operational
- response := httptest.NewRecorder()
- supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
- require.Equal(t, http.StatusOK, response.Result().StatusCode)
-
- // Crash the plugin
- supervisor.Hooks().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/plugins/id", nil))
-
- // Wait for it to potentially recover
- recovered := false
- for i := 0; i < 125; i++ {
- response := httptest.NewRecorder()
- supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
- if response.Result().StatusCode == http.StatusOK {
- recovered = true
- break
- }
-
- time.Sleep(time.Millisecond * 100)
- }
-
- if attempt < 4 {
- require.Nil(t, supervisorWaitErr)
- require.True(t, recovered, "failed to recover after attempt %d", attempt)
- } else {
- require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt)
- }
- }
-
- select {
- case <-supervisorWaitDone:
- require.NotNil(t, supervisorWaitErr)
- case <-time.After(500 * time.Millisecond):
- require.Fail(t, "supervisor.Wait() failed to return after plugin crashed")
- }
-
- require.NoError(t, supervisor.Stop())
-}
diff --git a/plugin/rpcplugin/sandbox/main_test.go b/plugin/rpcplugin/sandbox/main_test.go
deleted file mode 100644
index 4be4a42af..000000000
--- a/plugin/rpcplugin/sandbox/main_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package sandbox
-
-import (
- "testing"
-
- "github.com/mattermost/mattermost-server/mlog"
-)
-
-func TestMain(t *testing.T) {
- // Setup a global logger to catch tests logging outside of app context
- // The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
- mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
- EnableConsole: true,
- ConsoleJson: true,
- ConsoleLevel: "error",
- EnableFile: false,
- }))
-}
diff --git a/plugin/rpcplugin/sandbox/sandbox.go b/plugin/rpcplugin/sandbox/sandbox.go
deleted file mode 100644
index 96eff02dd..000000000
--- a/plugin/rpcplugin/sandbox/sandbox.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "context"
- "io"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-type MountPoint struct {
- Source string
- Destination string
- Type string
- ReadOnly bool
-}
-
-type Configuration struct {
- MountPoints []*MountPoint
- WorkingDirectory string
-}
-
-// NewProcess is like rpcplugin.NewProcess, but launches the process in a sandbox.
-func NewProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) {
- return newProcess(ctx, config, path)
-}
-
-// CheckSupport inspects the platform and environment to determine whether or not there are any
-// expected issues with sandboxing. If nil is returned, sandboxing should be used.
-func CheckSupport() error {
- return checkSupport()
-}
diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go
deleted file mode 100644
index beb00995d..000000000
--- a/plugin/rpcplugin/sandbox/sandbox_linux.go
+++ /dev/null
@@ -1,488 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "os/exec"
- "path/filepath"
- "syscall"
- "unsafe"
-
- "github.com/pkg/errors"
- "golang.org/x/sys/unix"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-func init() {
- if len(os.Args) < 4 || os.Args[0] != "sandbox.runProcess" {
- return
- }
-
- var config Configuration
- if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil {
- fmt.Println(err.Error())
- os.Exit(1)
- }
- if err := runProcess(&config, os.Args[2], os.Args[3]); err != nil {
- if eerr, ok := err.(*exec.ExitError); ok {
- if status, ok := eerr.Sys().(syscall.WaitStatus); ok {
- os.Exit(status.ExitStatus())
- }
- }
- fmt.Println(err.Error())
- os.Exit(1)
- }
- os.Exit(0)
-}
-
-func systemMountPoints() (points []*MountPoint) {
- points = append(points, &MountPoint{
- Source: "proc",
- Destination: "/proc",
- Type: "proc",
- }, &MountPoint{
- Source: "/dev/null",
- Destination: "/dev/null",
- }, &MountPoint{
- Source: "/dev/zero",
- Destination: "/dev/zero",
- }, &MountPoint{
- Source: "/dev/full",
- Destination: "/dev/full",
- })
-
- readOnly := []string{
- "/dev/random",
- "/dev/urandom",
- "/etc/resolv.conf",
- "/lib",
- "/lib32",
- "/lib64",
- "/usr/lib",
- "/usr/lib32",
- "/usr/lib64",
- "/etc/ca-certificates",
- "/etc/ssl/certs",
- "/system/etc/security/cacerts",
- "/usr/local/share/certs",
- "/etc/pki/tls/certs",
- "/etc/openssl/certs",
- "/etc/ssl/ca-bundle.pem",
- "/etc/pki/tls/cacert.pem",
- "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
- }
-
- for _, v := range []string{"SSL_CERT_FILE", "SSL_CERT_DIR"} {
- if path := os.Getenv(v); path != "" {
- readOnly = append(readOnly, path)
- }
- }
-
- for _, point := range readOnly {
- points = append(points, &MountPoint{
- Source: point,
- Destination: point,
- ReadOnly: true,
- })
- }
-
- return
-}
-
-func runProcess(config *Configuration, path, root string) error {
- if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
- return errors.Wrapf(err, "unable to make root private")
- }
-
- if err := mountMountPoints(root, systemMountPoints()); err != nil {
- return errors.Wrapf(err, "unable to mount sandbox system mount points")
- }
-
- if err := mountMountPoints(root, config.MountPoints); err != nil {
- return errors.Wrapf(err, "unable to mount sandbox config mount points")
- }
-
- if err := pivotRoot(root); err != nil {
- return errors.Wrapf(err, "unable to pivot sandbox root")
- }
-
- if err := os.Mkdir("/tmp", 0755); err != nil {
- return errors.Wrapf(err, "unable to create /tmp")
- }
-
- if config.WorkingDirectory != "" {
- if err := os.Chdir(config.WorkingDirectory); err != nil {
- return errors.Wrapf(err, "unable to set working directory")
- }
- }
-
- if err := dropInheritableCapabilities(); err != nil {
- return errors.Wrapf(err, "unable to drop inheritable capabilities")
- }
-
- if err := enableSeccompFilter(); err != nil {
- return errors.Wrapf(err, "unable to enable seccomp filter")
- }
-
- return runExecutable(path)
-}
-
-func mountMountPoint(root string, mountPoint *MountPoint) error {
- isDir := true
- if mountPoint.Type == "" {
- stat, err := os.Lstat(mountPoint.Source)
- if err != nil {
- return nil
- }
- if (stat.Mode() & os.ModeSymlink) != 0 {
- if path, err := filepath.EvalSymlinks(mountPoint.Source); err == nil {
- newMountPoint := *mountPoint
- newMountPoint.Source = path
- if err := mountMountPoint(root, &newMountPoint); err != nil {
- return errors.Wrapf(err, "unable to mount symbolic link target: "+mountPoint.Source)
- }
- return nil
- }
- }
- isDir = stat.IsDir()
- }
-
- target := filepath.Join(root, mountPoint.Destination)
-
- if isDir {
- if err := os.MkdirAll(target, 0755); err != nil {
- return errors.Wrapf(err, "unable to create directory: "+target)
- }
- } else {
- if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
- return errors.Wrapf(err, "unable to create directory: "+target)
- }
- f, err := os.Create(target)
- if err != nil {
- return errors.Wrapf(err, "unable to create file: "+target)
- }
- f.Close()
- }
-
- flags := uintptr(syscall.MS_NOSUID | syscall.MS_NODEV)
- if mountPoint.Type == "" {
- flags |= syscall.MS_BIND
- }
- if mountPoint.ReadOnly {
- flags |= syscall.MS_RDONLY
- }
-
- if err := syscall.Mount(mountPoint.Source, target, mountPoint.Type, flags, ""); err != nil {
- return errors.Wrapf(err, "unable to mount "+mountPoint.Source)
- }
-
- if (flags & syscall.MS_BIND) != 0 {
- // If this was a bind mount, our other flags actually got silently ignored during the above syscall:
- //
- // If mountflags includes MS_BIND [...] The remaining bits in the mountflags argument are
- // also ignored, with the exception of MS_REC.
- //
- // Furthermore, remounting will fail if we attempt to unset a bit that was inherited from
- // the mount's parent:
- //
- // The mount(2) flags MS_RDONLY, MS_NOSUID, MS_NOEXEC, and the "atime" flags
- // (MS_NOATIME, MS_NODIRATIME, MS_RELATIME) settings become locked when propagated from
- // a more privileged to a less privileged mount namespace, and may not be changed in the
- // less privileged mount namespace.
- //
- // So we need to get the actual flags, add our new ones, then do a remount if needed.
- var stats syscall.Statfs_t
- if err := syscall.Statfs(target, &stats); err != nil {
- return errors.Wrap(err, "unable to get mount flags for target: "+target)
- }
- const lockedFlagsMask = unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NOEXEC | unix.MS_NOATIME | unix.MS_NODIRATIME | unix.MS_RELATIME
- lockedFlags := uintptr(stats.Flags & lockedFlagsMask)
- if lockedFlags != ((flags | lockedFlags) & lockedFlagsMask) {
- if err := syscall.Mount("", target, "", flags|lockedFlags|syscall.MS_REMOUNT, ""); err != nil {
- return errors.Wrapf(err, "unable to remount "+mountPoint.Source)
- }
- }
- }
-
- return nil
-}
-
-func mountMountPoints(root string, mountPoints []*MountPoint) error {
- for _, mountPoint := range mountPoints {
- if err := mountMountPoint(root, mountPoint); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func pivotRoot(newRoot string) error {
- if err := syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
- return errors.Wrapf(err, "unable to mount new root")
- }
-
- prevRoot := filepath.Join(newRoot, ".prev_root")
-
- if err := os.MkdirAll(prevRoot, 0700); err != nil {
- return errors.Wrapf(err, "unable to create directory for previous root")
- }
-
- if err := syscall.PivotRoot(newRoot, prevRoot); err != nil {
- return errors.Wrapf(err, "syscall error")
- }
-
- if err := os.Chdir("/"); err != nil {
- return errors.Wrapf(err, "unable to change directory")
- }
-
- prevRoot = "/.prev_root"
-
- if err := syscall.Unmount(prevRoot, syscall.MNT_DETACH); err != nil {
- return errors.Wrapf(err, "unable to unmount previous root")
- }
-
- if err := os.RemoveAll(prevRoot); err != nil {
- return errors.Wrapf(err, "unable to remove previous root directory")
- }
-
- return nil
-}
-
-func dropInheritableCapabilities() error {
- type capHeader struct {
- version uint32
- pid int32
- }
-
- type capData struct {
- effective uint32
- permitted uint32
- inheritable uint32
- }
-
- var hdr capHeader
- var data [2]capData
-
- if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), 0, 0); errno != 0 {
- return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities version")
- }
-
- if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 {
- return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities")
- }
-
- data[0].inheritable = 0
- data[1].inheritable = 0
- if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 {
- return errors.Wrapf(syscall.Errno(errno), "unable to set inheritable capabilities")
- }
-
- for i := 0; i < 64; i++ {
- if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, uintptr(i), 0); errno != 0 && errno != syscall.EINVAL {
- return errors.Wrapf(syscall.Errno(errno), "unable to drop bounding set capability")
- }
- }
-
- return nil
-}
-
-func enableSeccompFilter() error {
- return EnableSeccompFilter(SeccompFilter(NATIVE_AUDIT_ARCH, AllowedSyscalls))
-}
-
-func runExecutable(path string) error {
- childFiles := []*os.File{
- os.NewFile(3, ""), os.NewFile(4, ""),
- }
- defer childFiles[0].Close()
- defer childFiles[1].Close()
-
- cmd := exec.Command(path)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.ExtraFiles = childFiles
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Pdeathsig: syscall.SIGTERM,
- }
-
- if err := cmd.Run(); err != nil {
- return err
- }
-
- return nil
-}
-
-type process struct {
- command *exec.Cmd
- root string
-}
-
-func newProcess(ctx context.Context, config *Configuration, path string) (pOut rpcplugin.Process, rwcOut io.ReadWriteCloser, errOut error) {
- configJSON, err := json.Marshal(config)
- if err != nil {
- return nil, nil, err
- }
-
- ipc, childFiles, err := rpcplugin.NewIPC()
- if err != nil {
- return nil, nil, err
- }
- defer childFiles[0].Close()
- defer childFiles[1].Close()
-
- root, err := ioutil.TempDir("", "")
- if err != nil {
- return nil, nil, err
- }
- defer func() {
- if errOut != nil {
- os.RemoveAll(root)
- }
- }()
-
- cmd := exec.CommandContext(ctx, "/proc/self/exe")
- cmd.Args = []string{"sandbox.runProcess", string(configJSON), path, root}
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.ExtraFiles = childFiles
-
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
- Pdeathsig: syscall.SIGTERM,
- GidMappings: []syscall.SysProcIDMap{
- {
- ContainerID: 0,
- HostID: os.Getgid(),
- Size: 1,
- },
- },
- UidMappings: []syscall.SysProcIDMap{
- {
- ContainerID: 0,
- HostID: os.Getuid(),
- Size: 1,
- },
- },
- }
-
- err = cmd.Start()
- if err != nil {
- ipc.Close()
- return nil, nil, err
- }
-
- return &process{
- command: cmd,
- root: root,
- }, ipc, nil
-}
-
-func (p *process) Wait() error {
- defer os.RemoveAll(p.root)
- return p.command.Wait()
-}
-
-func init() {
- if len(os.Args) < 2 || os.Args[0] != "sandbox.checkSupportInNamespace" {
- return
- }
-
- if err := checkSupportInNamespace(os.Args[1]); err != nil {
- fmt.Fprintf(os.Stderr, "%v", err.Error())
- os.Exit(1)
- }
-
- os.Exit(0)
-}
-
-func checkSupportInNamespace(root string) error {
- if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
- return errors.Wrapf(err, "unable to make root private")
- }
-
- if err := mountMountPoints(root, systemMountPoints()); err != nil {
- return errors.Wrapf(err, "unable to mount sandbox system mount points")
- }
-
- if err := pivotRoot(root); err != nil {
- return errors.Wrapf(err, "unable to pivot sandbox root")
- }
-
- if err := dropInheritableCapabilities(); err != nil {
- return errors.Wrapf(err, "unable to drop inheritable capabilities")
- }
-
- if err := enableSeccompFilter(); err != nil {
- return errors.Wrapf(err, "unable to enable seccomp filter")
- }
-
- if f, err := os.Create(os.DevNull); err != nil {
- return errors.Wrapf(err, "unable to open os.DevNull")
- } else {
- defer f.Close()
- if _, err = f.Write([]byte("foo")); err != nil {
- return errors.Wrapf(err, "unable to write to os.DevNull")
- }
- }
-
- return nil
-}
-
-func checkSupport() error {
- if AllowedSyscalls == nil {
- return fmt.Errorf("unsupported architecture")
- }
-
- stderr := &bytes.Buffer{}
-
- root, err := ioutil.TempDir("", "")
- if err != nil {
- return err
- }
- defer os.RemoveAll(root)
-
- cmd := exec.Command("/proc/self/exe")
- cmd.Args = []string{"sandbox.checkSupportInNamespace", root}
- cmd.Stderr = stderr
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
- Pdeathsig: syscall.SIGTERM,
- GidMappings: []syscall.SysProcIDMap{
- {
- ContainerID: 0,
- HostID: os.Getgid(),
- Size: 1,
- },
- },
- UidMappings: []syscall.SysProcIDMap{
- {
- ContainerID: 0,
- HostID: os.Getuid(),
- Size: 1,
- },
- },
- }
-
- if err := cmd.Start(); err != nil {
- return errors.Wrapf(err, "unable to create user namespace")
- }
-
- if err := cmd.Wait(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- return errors.Wrapf(fmt.Errorf("%v", stderr.String()), "unable to prepare namespace")
- }
- return errors.Wrapf(err, "unable to prepare namespace")
- }
-
- return nil
-}
diff --git a/plugin/rpcplugin/sandbox/sandbox_linux_test.go b/plugin/rpcplugin/sandbox/sandbox_linux_test.go
deleted file mode 100644
index 2bcbf0c57..000000000
--- a/plugin/rpcplugin/sandbox/sandbox_linux_test.go
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "context"
- "io/ioutil"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestNewProcess(t *testing.T) {
- if err := CheckSupport(); err != nil {
- t.Skip("sandboxing not supported:", err)
- }
-
- dir, err := ioutil.TempDir("", "")
- require.NoError(t, err)
- defer os.RemoveAll(dir)
-
- ping := filepath.Join(dir, "ping.exe")
- rpcplugintest.CompileGo(t, `
- package main
-
- import (
- "crypto/rand"
- "fmt"
- "io/ioutil"
- "net/http"
- "os"
- "os/exec"
- "syscall"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
- )
-
- var failures int
-
- type T struct {}
- func (T) Errorf(format string, args ...interface{}) {
- fmt.Printf(format, args...)
- failures++
- }
- func (T) FailNow() {
- os.Exit(1)
- }
-
- func init() {
- if len(os.Args) > 0 && os.Args[0] == "exitImmediately" {
- os.Exit(0)
- }
- }
-
- func main() {
- t := &T{}
-
- pwd, err := os.Getwd()
- assert.NoError(t, err)
- assert.Equal(t, "/dir", pwd)
-
- assert.Equal(t, 0, os.Getgid(), "we should see ourselves as root")
- assert.Equal(t, 0, os.Getuid(), "we should see ourselves as root")
-
- f, err := ioutil.TempFile("", "")
- require.NoError(t, err, "we should be able to create temporary files")
- f.Close()
-
- _, err = os.Stat("ping.exe")
- assert.NoError(t, err, "we should be able to read files in the working directory")
-
- buf := make([]byte, 20)
- n, err := rand.Read(buf)
- assert.Equal(t, 20, n)
- assert.NoError(t, err, "we should be able to read from /dev/urandom")
-
- f, err = os.Create("/dev/zero")
- require.NoError(t, err, "we should be able to write to /dev/zero")
- defer f.Close()
- n, err = f.Write([]byte("foo"))
- assert.Equal(t, 3, n)
- require.NoError(t, err, "we should be able to write to /dev/zero")
-
- f, err = os.Create("/dir/foo")
- if f != nil {
- defer f.Close()
- }
- assert.Error(t, err, "we shouldn't be able to write to this read-only mount point")
-
- _, err = ioutil.ReadFile("/etc/resolv.conf")
- require.NoError(t, err, "we should be able to read /etc/resolv.conf")
-
- resp, err := http.Get("https://github.com")
- require.NoError(t, err, "we should be able to use the network")
- resp.Body.Close()
-
- status, err := ioutil.ReadFile("/proc/self/status")
- require.NoError(t, err, "we should be able to read from /proc")
- assert.Regexp(t, status, "CapEff:\\s+0000000000000000", "we should have no effective capabilities")
-
- require.NoError(t, os.MkdirAll("/tmp/dir2", 0755))
- err = syscall.Mount("/dir", "/tmp/dir2", "", syscall.MS_BIND, "")
- assert.Equal(t, syscall.EPERM, err, "we shouldn't be allowed to mount things")
-
- cmd := exec.Command("/proc/self/exe")
- cmd.Args = []string{"exitImmediately"}
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Pdeathsig: syscall.SIGTERM,
- }
- assert.NoError(t, cmd.Run(), "we should be able to re-exec ourself")
-
- cmd = exec.Command("/proc/self/exe")
- cmd.Args = []string{"exitImmediately"}
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
- Pdeathsig: syscall.SIGTERM,
- }
- assert.Error(t, cmd.Run(), "we shouldn't be able to create new namespaces anymore")
-
- ipc, err := rpcplugin.InheritedProcessIPC()
- require.NoError(t, err)
- defer ipc.Close()
- _, err = ipc.Write([]byte("ping"))
- require.NoError(t, err)
-
- if failures > 0 {
- os.Exit(1)
- }
- }
- `, ping)
-
- p, ipc, err := NewProcess(context.Background(), &Configuration{
- MountPoints: []*MountPoint{
- {
- Source: dir,
- Destination: "/dir",
- ReadOnly: true,
- },
- },
- WorkingDirectory: "/dir",
- }, "/dir/ping.exe")
- require.NoError(t, err)
- defer ipc.Close()
- b := make([]byte, 10)
- n, err := ipc.Read(b)
- require.NoError(t, err)
- assert.Equal(t, 4, n)
- assert.Equal(t, "ping", string(b[:4]))
- require.NoError(t, p.Wait())
-}
diff --git a/plugin/rpcplugin/sandbox/sandbox_other.go b/plugin/rpcplugin/sandbox/sandbox_other.go
deleted file mode 100644
index 3889ecdcc..000000000
--- a/plugin/rpcplugin/sandbox/sandbox_other.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-// +build !linux
-
-package sandbox
-
-import (
- "context"
- "fmt"
- "io"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) {
- return nil, nil, checkSupport()
-}
-
-func checkSupport() error {
- return fmt.Errorf("sandboxing is not supported on this platform")
-}
diff --git a/plugin/rpcplugin/sandbox/sandbox_test.go b/plugin/rpcplugin/sandbox/sandbox_test.go
deleted file mode 100644
index e0149e28d..000000000
--- a/plugin/rpcplugin/sandbox/sandbox_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "testing"
-)
-
-// TestCheckSupport is here for debugging purposes and has no assertions. You can quickly test
-// sandboxing support with various systems by compiling the test executable and running this test on
-// your target systems. For example, with docker, executed from the root of the repo:
-//
-// docker run --rm -it -w /go/src/github.com/mattermost/mattermost-server
-// -v $(pwd):/go/src/github.com/mattermost/mattermost-server golang:1.9
-// go test -c ./plugin/rpcplugin
-//
-// docker run --rm -it --privileged -w /opt/mattermost
-// -v $(pwd):/opt/mattermost centos:6
-// ./rpcplugin.test --test.v --test.run TestCheckSupport
-func TestCheckSupport(t *testing.T) {
- if err := CheckSupport(); err != nil {
- t.Log(err.Error())
- }
-}
diff --git a/plugin/rpcplugin/sandbox/seccomp_linux.go b/plugin/rpcplugin/sandbox/seccomp_linux.go
deleted file mode 100644
index afe86e90a..000000000
--- a/plugin/rpcplugin/sandbox/seccomp_linux.go
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "syscall"
- "unsafe"
-
- "github.com/pkg/errors"
- "golang.org/x/net/bpf"
- "golang.org/x/sys/unix"
-)
-
-const (
- SECCOMP_RET_ALLOW = 0x7fff0000
- SECCOMP_RET_ERRNO = 0x00050000
-)
-
-const (
- EM_X86_64 = 62
-
- __AUDIT_ARCH_64BIT = 0x80000000
- __AUDIT_ARCH_LE = 0x40000000
-
- AUDIT_ARCH_X86_64 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE
-
- nrSize = 4
- archOffset = nrSize
- ipOffset = archOffset + 4
- argsOffset = ipOffset + 8
-)
-
-type SeccompCondition interface {
- Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction
-}
-
-func seccompArgLowWord(arg int, littleEndian bool) uint32 {
- offset := uint32(argsOffset + arg*8)
- if !littleEndian {
- offset += 4
- }
- return offset
-}
-
-func seccompArgHighWord(arg int, littleEndian bool) uint32 {
- offset := uint32(argsOffset + arg*8)
- if littleEndian {
- offset += 4
- }
- return offset
-}
-
-type SeccompArgHasNoBits struct {
- Arg int
- Mask uint64
-}
-
-func (c SeccompArgHasNoBits) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
- return []bpf.Instruction{
- bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: skipFalseSentinel},
- bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipTrue: skipFalseSentinel},
- }
-}
-
-type SeccompArgHasAnyBit struct {
- Arg int
- Mask uint64
-}
-
-func (c SeccompArgHasAnyBit) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
- return []bpf.Instruction{
- bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: 2},
- bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipFalse: skipFalseSentinel},
- }
-}
-
-type SeccompArgEquals struct {
- Arg int
- Value uint64
-}
-
-func (c SeccompArgEquals) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
- return []bpf.Instruction{
- bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value >> 32), SkipFalse: skipFalseSentinel},
- bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
- bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value), SkipFalse: skipFalseSentinel},
- }
-}
-
-type SeccompConditions struct {
- All []SeccompCondition
-}
-
-type SeccompSyscall struct {
- Syscall uint32
- Any []SeccompConditions
-}
-
-func SeccompFilter(arch uint32, allowedSyscalls []SeccompSyscall) (filter []bpf.Instruction) {
- filter = append(filter,
- bpf.LoadAbsolute{Off: archOffset, Size: 4},
- bpf.JumpIf{Cond: bpf.JumpEqual, Val: arch, SkipTrue: 1},
- bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)},
- )
-
- filter = append(filter, bpf.LoadAbsolute{Off: 0, Size: nrSize})
- for _, s := range allowedSyscalls {
- if s.Any != nil {
- syscallStart := len(filter)
- filter = append(filter, bpf.Instruction(nil))
- for _, cs := range s.Any {
- anyStart := len(filter)
- for _, c := range cs.All {
- filter = append(filter, c.Filter((arch&__AUDIT_ARCH_LE) != 0, 255)...)
- }
- filter = append(filter, bpf.RetConstant{Val: SECCOMP_RET_ALLOW})
- for i := anyStart; i < len(filter); i++ {
- if jump, ok := filter[i].(bpf.JumpIf); ok {
- if len(filter)-i-1 > 255 {
- panic("condition too long")
- }
- if jump.SkipFalse == 255 {
- jump.SkipFalse = uint8(len(filter) - i - 1)
- }
- if jump.SkipTrue == 255 {
- jump.SkipTrue = uint8(len(filter) - i - 1)
- }
- filter[i] = jump
- }
- }
- }
- filter = append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)})
- if len(filter)-syscallStart-1 > 255 {
- panic("conditions too long")
- }
- filter[syscallStart] = bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: uint8(len(filter) - syscallStart - 1)}
- } else {
- filter = append(filter,
- bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: 1},
- bpf.RetConstant{Val: SECCOMP_RET_ALLOW},
- )
- }
- }
-
- return append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)})
-}
-
-func EnableSeccompFilter(filter []bpf.Instruction) error {
- assembled, err := bpf.Assemble(filter)
- if err != nil {
- return errors.Wrapf(err, "unable to assemble filter")
- }
-
- sockFilter := make([]unix.SockFilter, len(filter))
- for i, instruction := range assembled {
- sockFilter[i].Code = instruction.Op
- sockFilter[i].Jt = instruction.Jt
- sockFilter[i].Jf = instruction.Jf
- sockFilter[i].K = instruction.K
- }
-
- prog := unix.SockFprog{
- Len: uint16(len(sockFilter)),
- Filter: &sockFilter[0],
- }
-
- if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&prog))); errno != 0 {
- return errors.Wrapf(syscall.Errno(errno), "syscall error")
- }
-
- return nil
-}
diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go b/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go
deleted file mode 100644
index 7338ebbe0..000000000
--- a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go
+++ /dev/null
@@ -1,301 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "golang.org/x/sys/unix"
-)
-
-const NATIVE_AUDIT_ARCH = AUDIT_ARCH_X86_64
-
-var AllowedSyscalls = []SeccompSyscall{
- {Syscall: unix.SYS_ACCEPT},
- {Syscall: unix.SYS_ACCEPT4},
- {Syscall: unix.SYS_ACCESS},
- {Syscall: unix.SYS_ADJTIMEX},
- {Syscall: unix.SYS_ALARM},
- {Syscall: unix.SYS_ARCH_PRCTL},
- {Syscall: unix.SYS_BIND},
- {Syscall: unix.SYS_BRK},
- {Syscall: unix.SYS_CAPGET},
- {Syscall: unix.SYS_CAPSET},
- {Syscall: unix.SYS_CHDIR},
- {Syscall: unix.SYS_CHMOD},
- {Syscall: unix.SYS_CHOWN},
- {Syscall: unix.SYS_CLOCK_GETRES},
- {Syscall: unix.SYS_CLOCK_GETTIME},
- {Syscall: unix.SYS_CLOCK_NANOSLEEP},
- {
- Syscall: unix.SYS_CLONE,
- Any: []SeccompConditions{{
- All: []SeccompCondition{SeccompArgHasNoBits{
- Arg: 0,
- Mask: unix.CLONE_NEWCGROUP | unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS,
- }},
- }},
- },
- {Syscall: unix.SYS_CLOSE},
- {Syscall: unix.SYS_CONNECT},
- {Syscall: unix.SYS_COPY_FILE_RANGE},
- {Syscall: unix.SYS_CREAT},
- {Syscall: unix.SYS_DUP},
- {Syscall: unix.SYS_DUP2},
- {Syscall: unix.SYS_DUP3},
- {Syscall: unix.SYS_EPOLL_CREATE},
- {Syscall: unix.SYS_EPOLL_CREATE1},
- {Syscall: unix.SYS_EPOLL_CTL},
- {Syscall: unix.SYS_EPOLL_CTL_OLD},
- {Syscall: unix.SYS_EPOLL_PWAIT},
- {Syscall: unix.SYS_EPOLL_WAIT},
- {Syscall: unix.SYS_EPOLL_WAIT_OLD},
- {Syscall: unix.SYS_EVENTFD},
- {Syscall: unix.SYS_EVENTFD2},
- {Syscall: unix.SYS_EXECVE},
- {Syscall: unix.SYS_EXECVEAT},
- {Syscall: unix.SYS_EXIT},
- {Syscall: unix.SYS_EXIT_GROUP},
- {Syscall: unix.SYS_FACCESSAT},
- {Syscall: unix.SYS_FADVISE64},
- {Syscall: unix.SYS_FALLOCATE},
- {Syscall: unix.SYS_FANOTIFY_MARK},
- {Syscall: unix.SYS_FCHDIR},
- {Syscall: unix.SYS_FCHMOD},
- {Syscall: unix.SYS_FCHMODAT},
- {Syscall: unix.SYS_FCHOWN},
- {Syscall: unix.SYS_FCHOWNAT},
- {Syscall: unix.SYS_FCNTL},
- {Syscall: unix.SYS_FDATASYNC},
- {Syscall: unix.SYS_FGETXATTR},
- {Syscall: unix.SYS_FLISTXATTR},
- {Syscall: unix.SYS_FLOCK},
- {Syscall: unix.SYS_FORK},
- {Syscall: unix.SYS_FREMOVEXATTR},
- {Syscall: unix.SYS_FSETXATTR},
- {Syscall: unix.SYS_FSTAT},
- {Syscall: unix.SYS_FSTATFS},
- {Syscall: unix.SYS_FSYNC},
- {Syscall: unix.SYS_FTRUNCATE},
- {Syscall: unix.SYS_FUTEX},
- {Syscall: unix.SYS_FUTIMESAT},
- {Syscall: unix.SYS_GETCPU},
- {Syscall: unix.SYS_GETCWD},
- {Syscall: unix.SYS_GETDENTS},
- {Syscall: unix.SYS_GETDENTS64},
- {Syscall: unix.SYS_GETEGID},
- {Syscall: unix.SYS_GETEUID},
- {Syscall: unix.SYS_GETGID},
- {Syscall: unix.SYS_GETGROUPS},
- {Syscall: unix.SYS_GETITIMER},
- {Syscall: unix.SYS_GETPEERNAME},
- {Syscall: unix.SYS_GETPGID},
- {Syscall: unix.SYS_GETPGRP},
- {Syscall: unix.SYS_GETPID},
- {Syscall: unix.SYS_GETPPID},
- {Syscall: unix.SYS_GETPRIORITY},
- {Syscall: unix.SYS_GETRANDOM},
- {Syscall: unix.SYS_GETRESGID},
- {Syscall: unix.SYS_GETRESUID},
- {Syscall: unix.SYS_GETRLIMIT},
- {Syscall: unix.SYS_GET_ROBUST_LIST},
- {Syscall: unix.SYS_GETRUSAGE},
- {Syscall: unix.SYS_GETSID},
- {Syscall: unix.SYS_GETSOCKNAME},
- {Syscall: unix.SYS_GETSOCKOPT},
- {Syscall: unix.SYS_GET_THREAD_AREA},
- {Syscall: unix.SYS_GETTID},
- {Syscall: unix.SYS_GETTIMEOFDAY},
- {Syscall: unix.SYS_GETUID},
- {Syscall: unix.SYS_GETXATTR},
- {Syscall: unix.SYS_INOTIFY_ADD_WATCH},
- {Syscall: unix.SYS_INOTIFY_INIT},
- {Syscall: unix.SYS_INOTIFY_INIT1},
- {Syscall: unix.SYS_INOTIFY_RM_WATCH},
- {Syscall: unix.SYS_IO_CANCEL},
- {Syscall: unix.SYS_IOCTL},
- {Syscall: unix.SYS_IO_DESTROY},
- {Syscall: unix.SYS_IO_GETEVENTS},
- {Syscall: unix.SYS_IOPRIO_GET},
- {Syscall: unix.SYS_IOPRIO_SET},
- {Syscall: unix.SYS_IO_SETUP},
- {Syscall: unix.SYS_IO_SUBMIT},
- {Syscall: unix.SYS_KILL},
- {Syscall: unix.SYS_LCHOWN},
- {Syscall: unix.SYS_LGETXATTR},
- {Syscall: unix.SYS_LINK},
- {Syscall: unix.SYS_LINKAT},
- {Syscall: unix.SYS_LISTEN},
- {Syscall: unix.SYS_LISTXATTR},
- {Syscall: unix.SYS_LLISTXATTR},
- {Syscall: unix.SYS_LREMOVEXATTR},
- {Syscall: unix.SYS_LSEEK},
- {Syscall: unix.SYS_LSETXATTR},
- {Syscall: unix.SYS_LSTAT},
- {Syscall: unix.SYS_MADVISE},
- {Syscall: unix.SYS_MEMFD_CREATE},
- {Syscall: unix.SYS_MINCORE},
- {Syscall: unix.SYS_MKDIR},
- {Syscall: unix.SYS_MKDIRAT},
- {Syscall: unix.SYS_MKNOD},
- {Syscall: unix.SYS_MKNODAT},
- {Syscall: unix.SYS_MLOCK},
- {Syscall: unix.SYS_MLOCK2},
- {Syscall: unix.SYS_MLOCKALL},
- {Syscall: unix.SYS_MMAP},
- {Syscall: unix.SYS_MODIFY_LDT},
- {Syscall: unix.SYS_MPROTECT},
- {Syscall: unix.SYS_MQ_GETSETATTR},
- {Syscall: unix.SYS_MQ_NOTIFY},
- {Syscall: unix.SYS_MQ_OPEN},
- {Syscall: unix.SYS_MQ_TIMEDRECEIVE},
- {Syscall: unix.SYS_MQ_TIMEDSEND},
- {Syscall: unix.SYS_MQ_UNLINK},
- {Syscall: unix.SYS_MREMAP},
- {Syscall: unix.SYS_MSGCTL},
- {Syscall: unix.SYS_MSGGET},
- {Syscall: unix.SYS_MSGRCV},
- {Syscall: unix.SYS_MSGSND},
- {Syscall: unix.SYS_MSYNC},
- {Syscall: unix.SYS_MUNLOCK},
- {Syscall: unix.SYS_MUNLOCKALL},
- {Syscall: unix.SYS_MUNMAP},
- {Syscall: unix.SYS_NANOSLEEP},
- {Syscall: unix.SYS_NEWFSTATAT},
- {Syscall: unix.SYS_OPEN},
- {Syscall: unix.SYS_OPENAT},
- {Syscall: unix.SYS_PAUSE},
- {
- Syscall: unix.SYS_PERSONALITY,
- Any: []SeccompConditions{
- {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0}}},
- {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 8}}},
- {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20000}}},
- {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20008}}},
- {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0xffffffff}}},
- },
- },
- {Syscall: unix.SYS_PIPE},
- {Syscall: unix.SYS_PIPE2},
- {Syscall: unix.SYS_POLL},
- {Syscall: unix.SYS_PPOLL},
- {Syscall: unix.SYS_PRCTL},
- {Syscall: unix.SYS_PREAD64},
- {Syscall: unix.SYS_PREADV},
- {Syscall: unix.SYS_PREADV2},
- {Syscall: unix.SYS_PRLIMIT64},
- {Syscall: unix.SYS_PSELECT6},
- {Syscall: unix.SYS_PWRITE64},
- {Syscall: unix.SYS_PWRITEV},
- {Syscall: unix.SYS_PWRITEV2},
- {Syscall: unix.SYS_READ},
- {Syscall: unix.SYS_READAHEAD},
- {Syscall: unix.SYS_READLINK},
- {Syscall: unix.SYS_READLINKAT},
- {Syscall: unix.SYS_READV},
- {Syscall: unix.SYS_RECVFROM},
- {Syscall: unix.SYS_RECVMMSG},
- {Syscall: unix.SYS_RECVMSG},
- {Syscall: unix.SYS_REMAP_FILE_PAGES},
- {Syscall: unix.SYS_REMOVEXATTR},
- {Syscall: unix.SYS_RENAME},
- {Syscall: unix.SYS_RENAMEAT},
- {Syscall: unix.SYS_RENAMEAT2},
- {Syscall: unix.SYS_RESTART_SYSCALL},
- {Syscall: unix.SYS_RMDIR},
- {Syscall: unix.SYS_RT_SIGACTION},
- {Syscall: unix.SYS_RT_SIGPENDING},
- {Syscall: unix.SYS_RT_SIGPROCMASK},
- {Syscall: unix.SYS_RT_SIGQUEUEINFO},
- {Syscall: unix.SYS_RT_SIGRETURN},
- {Syscall: unix.SYS_RT_SIGSUSPEND},
- {Syscall: unix.SYS_RT_SIGTIMEDWAIT},
- {Syscall: unix.SYS_RT_TGSIGQUEUEINFO},
- {Syscall: unix.SYS_SCHED_GETAFFINITY},
- {Syscall: unix.SYS_SCHED_GETATTR},
- {Syscall: unix.SYS_SCHED_GETPARAM},
- {Syscall: unix.SYS_SCHED_GET_PRIORITY_MAX},
- {Syscall: unix.SYS_SCHED_GET_PRIORITY_MIN},
- {Syscall: unix.SYS_SCHED_GETSCHEDULER},
- {Syscall: unix.SYS_SCHED_RR_GET_INTERVAL},
- {Syscall: unix.SYS_SCHED_SETAFFINITY},
- {Syscall: unix.SYS_SCHED_SETATTR},
- {Syscall: unix.SYS_SCHED_SETPARAM},
- {Syscall: unix.SYS_SCHED_SETSCHEDULER},
- {Syscall: unix.SYS_SCHED_YIELD},
- {Syscall: unix.SYS_SECCOMP},
- {Syscall: unix.SYS_SELECT},
- {Syscall: unix.SYS_SEMCTL},
- {Syscall: unix.SYS_SEMGET},
- {Syscall: unix.SYS_SEMOP},
- {Syscall: unix.SYS_SEMTIMEDOP},
- {Syscall: unix.SYS_SENDFILE},
- {Syscall: unix.SYS_SENDMMSG},
- {Syscall: unix.SYS_SENDMSG},
- {Syscall: unix.SYS_SENDTO},
- {Syscall: unix.SYS_SETFSGID},
- {Syscall: unix.SYS_SETFSUID},
- {Syscall: unix.SYS_SETGID},
- {Syscall: unix.SYS_SETGROUPS},
- {Syscall: unix.SYS_SETITIMER},
- {Syscall: unix.SYS_SETPGID},
- {Syscall: unix.SYS_SETPRIORITY},
- {Syscall: unix.SYS_SETREGID},
- {Syscall: unix.SYS_SETRESGID},
- {Syscall: unix.SYS_SETRESUID},
- {Syscall: unix.SYS_SETREUID},
- {Syscall: unix.SYS_SETRLIMIT},
- {Syscall: unix.SYS_SET_ROBUST_LIST},
- {Syscall: unix.SYS_SETSID},
- {Syscall: unix.SYS_SETSOCKOPT},
- {Syscall: unix.SYS_SET_THREAD_AREA},
- {Syscall: unix.SYS_SET_TID_ADDRESS},
- {Syscall: unix.SYS_SETUID},
- {Syscall: unix.SYS_SETXATTR},
- {Syscall: unix.SYS_SHMAT},
- {Syscall: unix.SYS_SHMCTL},
- {Syscall: unix.SYS_SHMDT},
- {Syscall: unix.SYS_SHMGET},
- {Syscall: unix.SYS_SHUTDOWN},
- {Syscall: unix.SYS_SIGALTSTACK},
- {Syscall: unix.SYS_SIGNALFD},
- {Syscall: unix.SYS_SIGNALFD4},
- {Syscall: unix.SYS_SOCKET},
- {Syscall: unix.SYS_SOCKETPAIR},
- {Syscall: unix.SYS_SPLICE},
- {Syscall: unix.SYS_STAT},
- {Syscall: unix.SYS_STATFS},
- {Syscall: unix.SYS_SYMLINK},
- {Syscall: unix.SYS_SYMLINKAT},
- {Syscall: unix.SYS_SYNC},
- {Syscall: unix.SYS_SYNC_FILE_RANGE},
- {Syscall: unix.SYS_SYNCFS},
- {Syscall: unix.SYS_SYSINFO},
- {Syscall: unix.SYS_SYSLOG},
- {Syscall: unix.SYS_TEE},
- {Syscall: unix.SYS_TGKILL},
- {Syscall: unix.SYS_TIME},
- {Syscall: unix.SYS_TIMER_CREATE},
- {Syscall: unix.SYS_TIMER_DELETE},
- {Syscall: unix.SYS_TIMERFD_CREATE},
- {Syscall: unix.SYS_TIMERFD_GETTIME},
- {Syscall: unix.SYS_TIMERFD_SETTIME},
- {Syscall: unix.SYS_TIMER_GETOVERRUN},
- {Syscall: unix.SYS_TIMER_GETTIME},
- {Syscall: unix.SYS_TIMER_SETTIME},
- {Syscall: unix.SYS_TIMES},
- {Syscall: unix.SYS_TKILL},
- {Syscall: unix.SYS_TRUNCATE},
- {Syscall: unix.SYS_UMASK},
- {Syscall: unix.SYS_UNAME},
- {Syscall: unix.SYS_UNLINK},
- {Syscall: unix.SYS_UNLINKAT},
- {Syscall: unix.SYS_UTIME},
- {Syscall: unix.SYS_UTIMENSAT},
- {Syscall: unix.SYS_UTIMES},
- {Syscall: unix.SYS_VFORK},
- {Syscall: unix.SYS_VMSPLICE},
- {Syscall: unix.SYS_WAIT4},
- {Syscall: unix.SYS_WAITID},
- {Syscall: unix.SYS_WRITE},
- {Syscall: unix.SYS_WRITEV},
-}
diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_other.go b/plugin/rpcplugin/sandbox/seccomp_linux_other.go
deleted file mode 100644
index 5573943cd..000000000
--- a/plugin/rpcplugin/sandbox/seccomp_linux_other.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-// +build linux,!amd64
-
-package sandbox
-
-const NATIVE_AUDIT_ARCH = 0
-
-var AllowedSyscalls []SeccompSyscall
diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_test.go b/plugin/rpcplugin/sandbox/seccomp_linux_test.go
deleted file mode 100644
index 46fe38fe0..000000000
--- a/plugin/rpcplugin/sandbox/seccomp_linux_test.go
+++ /dev/null
@@ -1,210 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "encoding/binary"
- "syscall"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/net/bpf"
-)
-
-func seccompData(nr int32, arch uint32, ip uint64, args ...uint64) []byte {
- var buf [64]byte
- binary.BigEndian.PutUint32(buf[0:], uint32(nr))
- binary.BigEndian.PutUint32(buf[4:], arch)
- binary.BigEndian.PutUint64(buf[8:], ip)
- for i := 0; i < 6 && i < len(args); i++ {
- binary.BigEndian.PutUint64(buf[16+i*8:], args[i])
- }
- return buf[:]
-}
-
-func TestSeccompFilter(t *testing.T) {
- for name, tc := range map[string]struct {
- Filter []bpf.Instruction
- Data []byte
- Expected bool
- }{
- "Allowed": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {Syscall: syscall.SYS_READ},
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_READ, 0xf00, 0),
- Expected: true,
- },
- "AllFail": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {
- Syscall: syscall.SYS_READ,
- Any: []SeccompConditions{
- {All: []SeccompCondition{
- &SeccompArgHasAnyBit{Arg: 0, Mask: 2},
- &SeccompArgHasAnyBit{Arg: 1, Mask: 2},
- &SeccompArgHasAnyBit{Arg: 2, Mask: 2},
- &SeccompArgHasAnyBit{Arg: 3, Mask: 2},
- }},
- },
- },
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
- Expected: false,
- },
- "AllPass": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {
- Syscall: syscall.SYS_READ,
- Any: []SeccompConditions{
- {All: []SeccompCondition{
- &SeccompArgHasAnyBit{Arg: 0, Mask: 7},
- &SeccompArgHasAnyBit{Arg: 1, Mask: 7},
- &SeccompArgHasAnyBit{Arg: 2, Mask: 7},
- &SeccompArgHasAnyBit{Arg: 3, Mask: 7},
- }},
- },
- },
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
- Expected: true,
- },
- "AnyFail": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {
- Syscall: syscall.SYS_READ,
- Any: []SeccompConditions{
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 8}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 8}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 8}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 8}}},
- },
- },
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
- Expected: false,
- },
- "AnyPass": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {
- Syscall: syscall.SYS_READ,
- Any: []SeccompConditions{
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 2}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 2}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 2}}},
- {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 2}}},
- },
- },
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
- Expected: true,
- },
- "BadArch": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {Syscall: syscall.SYS_READ},
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_MOUNT, 0xf01, 0),
- Expected: false,
- },
- "BadSyscall": {
- Filter: SeccompFilter(0xf00, []SeccompSyscall{
- {Syscall: syscall.SYS_READ},
- {Syscall: syscall.SYS_WRITE},
- }),
- Data: seccompData(syscall.SYS_MOUNT, 0xf00, 0),
- Expected: false,
- },
- } {
- t.Run(name, func(t *testing.T) {
- vm, err := bpf.NewVM(tc.Filter)
- require.NoError(t, err)
- result, err := vm.Run(tc.Data)
- require.NoError(t, err)
- if tc.Expected {
- assert.Equal(t, SECCOMP_RET_ALLOW, result)
- } else {
- assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result)
- }
- })
- }
-}
-
-func TestSeccompFilter_Conditions(t *testing.T) {
- for name, tc := range map[string]struct {
- Condition SeccompCondition
- Args []uint64
- Expected bool
- }{
- "ArgHasAnyBitFail": {
- Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x0004},
- Args: []uint64{0x0400008000},
- Expected: false,
- },
- "ArgHasAnyBitPass1": {
- Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004},
- Args: []uint64{0x8000008004},
- Expected: true,
- },
- "ArgHasAnyBitPass2": {
- Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004},
- Args: []uint64{0x8400008000},
- Expected: true,
- },
- "ArgHasNoBitsFail1": {
- Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011},
- Args: []uint64{0x0000008007},
- Expected: false,
- },
- "ArgHasNoBitsFail2": {
- Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011},
- Args: []uint64{0x0700008000},
- Expected: false,
- },
- "ArgHasNoBitsPass": {
- Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x400000004},
- Args: []uint64{0x8000008000},
- Expected: true,
- },
- "ArgEqualsPass": {
- Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
- Args: []uint64{0x123456789ABCDEF},
- Expected: true,
- },
- "ArgEqualsFail1": {
- Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
- Args: []uint64{0x023456789ABCDEF},
- Expected: false,
- },
- "ArgEqualsFail2": {
- Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
- Args: []uint64{0x123456789ABCDE0},
- Expected: false,
- },
- } {
- t.Run(name, func(t *testing.T) {
- filter := SeccompFilter(0xf00, []SeccompSyscall{
- {
- Syscall: 1,
- Any: []SeccompConditions{{All: []SeccompCondition{tc.Condition}}},
- },
- })
- vm, err := bpf.NewVM(filter)
- require.NoError(t, err)
- result, err := vm.Run(seccompData(1, 0xf00, 0, tc.Args...))
- require.NoError(t, err)
- if tc.Expected {
- assert.Equal(t, SECCOMP_RET_ALLOW, result)
- } else {
- assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result)
- }
- })
- }
-}
diff --git a/plugin/rpcplugin/sandbox/supervisor.go b/plugin/rpcplugin/sandbox/supervisor.go
deleted file mode 100644
index 0e63954fd..000000000
--- a/plugin/rpcplugin/sandbox/supervisor.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "context"
- "fmt"
- "io"
- "path/filepath"
- "strings"
-
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
- "github.com/mattermost/mattermost-server/plugin/rpcplugin"
-)
-
-func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
- return rpcplugin.SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (rpcplugin.Process, io.ReadWriteCloser, error) {
- executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
- if strings.HasPrefix(executable, "..") {
- return nil, nil, fmt.Errorf("invalid backend executable")
- }
- return NewProcess(ctx, &Configuration{
- MountPoints: []*MountPoint{{
- Source: bundle.Path,
- Destination: "/plugin",
- ReadOnly: true,
- }},
- WorkingDirectory: "/plugin",
- }, filepath.Join("/plugin", executable))
- })
-}
diff --git a/plugin/rpcplugin/sandbox/supervisor_test.go b/plugin/rpcplugin/sandbox/supervisor_test.go
deleted file mode 100644
index 245185dd5..000000000
--- a/plugin/rpcplugin/sandbox/supervisor_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package sandbox
-
-import (
- "testing"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestSupervisorProvider(t *testing.T) {
- if err := CheckSupport(); err != nil {
- t.Skip("sandboxing not supported:", err)
- }
-
- rpcplugintest.TestSupervisorProvider(t, SupervisorProvider)
-}
diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go
deleted file mode 100644
index 246747c89..000000000
--- a/plugin/rpcplugin/supervisor.go
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "context"
- "fmt"
- "io"
- "path/filepath"
- "strings"
- "sync/atomic"
- "time"
-
- "github.com/mattermost/mattermost-server/mlog"
- "github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/plugin"
-)
-
-const (
- MaxProcessRestarts = 3
-)
-
-// Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and
-// communicates via RPC.
-//
-// If the plugin unexpectedly exits, the supervisor will relaunch it after a short delay, but will
-// only restart a plugin at most three times.
-type Supervisor struct {
- hooks atomic.Value
- done chan bool
- cancel context.CancelFunc
- newProcess func(context.Context) (Process, io.ReadWriteCloser, error)
- pluginId string
- pluginErr error
-}
-
-var _ plugin.Supervisor = (*Supervisor)(nil)
-
-// Starts the plugin. This method will block until the plugin is successfully launched for the first
-// time and will return an error if the plugin cannot be launched at all.
-func (s *Supervisor) Start(api plugin.API) error {
- ctx, cancel := context.WithCancel(context.Background())
- s.done = make(chan bool, 1)
- start := make(chan error, 1)
- go s.run(ctx, start, api)
-
- select {
- case <-time.After(time.Second * 3):
- cancel()
- <-s.done
- return fmt.Errorf("timed out waiting for plugin")
- case err := <-start:
- s.cancel = cancel
- return err
- }
-}
-
-// Waits for the supervisor to stop (on demand or of its own accord), returning any error that
-// triggered the supervisor to stop.
-func (s *Supervisor) Wait() error {
- <-s.done
- return s.pluginErr
-}
-
-// Stops the plugin.
-func (s *Supervisor) Stop() error {
- s.cancel()
- <-s.done
- return nil
-}
-
-// Returns the hooks used to communicate with the plugin. The hooks may change if the plugin is
-// restarted, so the return value should not be cached.
-func (s *Supervisor) Hooks() plugin.Hooks {
- return s.hooks.Load().(plugin.Hooks)
-}
-
-func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) {
- defer func() {
- close(s.done)
- }()
- done := ctx.Done()
- for i := 0; i <= MaxProcessRestarts; i++ {
- s.runPlugin(ctx, start, api)
- select {
- case <-done:
- return
- default:
- start = nil
- if i < MaxProcessRestarts {
- mlog.Error("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId))
- time.Sleep(time.Duration((1 + i*i)) * time.Second)
- } else {
- s.pluginErr = fmt.Errorf("plugin terminated unexpectedly too many times")
- mlog.Error("Plugin shutdown", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts), mlog.Err(s.pluginErr))
- }
- }
- }
-}
-
-func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plugin.API) error {
- if start == nil {
- mlog.Debug("Restarting plugin", mlog.String("plugin_id", s.pluginId))
- }
-
- p, ipc, err := s.newProcess(ctx)
- if err != nil {
- if start != nil {
- start <- err
- }
- return err
- }
-
- muxer := NewMuxer(ipc, false)
- closeMuxer := make(chan bool, 1)
- muxerClosed := make(chan error, 1)
- go func() {
- select {
- case <-ctx.Done():
- break
- case <-closeMuxer:
- break
- }
- muxerClosed <- muxer.Close()
- }()
-
- hooks, err := ConnectMain(muxer, s.pluginId)
- if err == nil {
- err = hooks.OnActivate(api)
- }
-
- if err != nil {
- if start != nil {
- start <- err
- }
- closeMuxer <- true
- <-muxerClosed
- p.Wait()
- return err
- }
-
- s.hooks.Store(hooks)
-
- if start != nil {
- start <- nil
- }
- p.Wait()
- closeMuxer <- true
- <-muxerClosed
-
- return nil
-}
-
-func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
- return SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (Process, io.ReadWriteCloser, error) {
- executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
- if strings.HasPrefix(executable, "..") {
- return nil, nil, fmt.Errorf("invalid backend executable")
- }
- return NewProcess(ctx, filepath.Join(bundle.Path, executable))
- })
-}
-
-func SupervisorWithNewProcessFunc(bundle *model.BundleInfo, newProcess func(context.Context) (Process, io.ReadWriteCloser, error)) (plugin.Supervisor, error) {
- if bundle.Manifest == nil {
- return nil, fmt.Errorf("no manifest available")
- } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" {
- return nil, fmt.Errorf("no backend executable specified")
- }
- executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
- if strings.HasPrefix(executable, "..") {
- return nil, fmt.Errorf("invalid backend executable")
- }
- return &Supervisor{pluginId: bundle.Manifest.Id, newProcess: newProcess}, nil
-}
diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go
deleted file mode 100644
index 06c1fafeb..000000000
--- a/plugin/rpcplugin/supervisor_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package rpcplugin
-
-import (
- "testing"
-
- "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
-)
-
-func TestSupervisorProvider(t *testing.T) {
- rpcplugintest.TestSupervisorProvider(t, SupervisorProvider)
-}
diff --git a/plugin/supervisor.go b/plugin/supervisor.go
index f20df7040..0471f7861 100644
--- a/plugin/supervisor.go
+++ b/plugin/supervisor.go
@@ -1,13 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
+// See LICENSE.txt for license information.
package plugin
-// Supervisor provides the interface for an object that controls the execution of a plugin. This
-// type is only relevant to the server, and isn't used by the plugins themselves.
-type Supervisor interface {
- Start(API) error
- Wait() error
- Stop() error
- Hooks() Hooks
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/go-plugin"
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+)
+
+type Supervisor struct {
+ pluginId string
+ client *plugin.Client
+ hooks Hooks
+ implemented [TotalHooksId]bool
+}
+
+func NewSupervisor(pluginInfo *model.BundleInfo, parentLogger *mlog.Logger, apiImpl API) (*Supervisor, error) {
+ supervisor := Supervisor{}
+
+ wrappedLogger := pluginInfo.WrapLogger(parentLogger)
+
+ hclogAdaptedLogger := &HclogAdapter{
+ wrappedLogger: wrappedLogger,
+ extrasKey: "wrapped_extras",
+ }
+
+ pluginMap := map[string]plugin.Plugin{
+ "hooks": &HooksPlugin{
+ log: wrappedLogger,
+ apiImpl: apiImpl,
+ },
+ }
+
+ executable := filepath.Clean(filepath.Join(".", pluginInfo.Manifest.Backend.Executable))
+ if strings.HasPrefix(executable, "..") {
+ return nil, fmt.Errorf("invalid backend executable")
+ }
+ executable = filepath.Join(pluginInfo.Path, executable)
+
+ supervisor.client = plugin.NewClient(&plugin.ClientConfig{
+ HandshakeConfig: Handshake,
+ Plugins: pluginMap,
+ Cmd: exec.Command(executable),
+ SyncStdout: os.Stdout,
+ SyncStderr: os.Stdout,
+ Logger: hclogAdaptedLogger,
+ StartTimeout: time.Second * 3,
+ })
+
+ rpcClient, err := supervisor.client.Client()
+ if err != nil {
+ return nil, err
+ }
+
+ raw, err := rpcClient.Dispense("hooks")
+ if err != nil {
+ return nil, err
+ }
+
+ supervisor.hooks = raw.(Hooks)
+
+ if impl, err := supervisor.hooks.Implemented(); err != nil {
+ return nil, err
+ } else {
+ for _, hookName := range impl {
+ if hookId, ok := HookNameToId[hookName]; ok {
+ supervisor.implemented[hookId] = true
+ }
+ }
+ }
+
+ err = supervisor.Hooks().OnActivate()
+ if err != nil {
+ return nil, err
+ }
+
+ return &supervisor, nil
+}
+
+func (sup *Supervisor) Shutdown() {
+ sup.client.Kill()
+}
+
+func (sup *Supervisor) Hooks() Hooks {
+ return sup.hooks
+}
+
+func (sup *Supervisor) Implements(hookId int) bool {
+ return sup.implemented[hookId]
}
diff --git a/plugin/supervisor_test.go b/plugin/supervisor_test.go
new file mode 100644
index 000000000..605835f68
--- /dev/null
+++ b/plugin/supervisor_test.go
@@ -0,0 +1,148 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package plugin
+
+import (
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/mattermost/mattermost-server/mlog"
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/mattermost/mattermost-server/plugin/plugintest"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSupervisor(t *testing.T) {
+ for name, f := range map[string]func(*testing.T){
+ "Supervisor": testSupervisor,
+ "Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath,
+ "Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath,
+ "Supervisor_StartTimeout": testSupervisor_StartTimeout,
+ } {
+ t.Run(name, f)
+ }
+}
+
+func CompileGo(t *testing.T, sourceCode, outputPath string) {
+ dir, err := ioutil.TempDir(".", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+ require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600))
+ cmd := exec.Command("go", "build", "-o", outputPath, "main.go")
+ cmd.Dir = dir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ require.NoError(t, cmd.Run())
+}
+
+func testSupervisor(t *testing.T) {
+ dir, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ backend := filepath.Join(dir, "backend.exe")
+ CompileGo(t, `
+ package main
+
+ import (
+ "github.com/mattermost/mattermost-server/plugin"
+ )
+
+ type MyPlugin struct {
+ plugin.MattermostPlugin
+ }
+
+ func main() {
+ plugin.ClientMain(&MyPlugin{})
+ }
+ `, backend)
+
+ ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
+
+ bundle := model.BundleInfoForPath(dir)
+ var api plugintest.API
+ api.On("LoadPluginConfiguration", mock.Anything).Return(nil)
+ log := mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: "error",
+ EnableFile: false,
+ })
+ supervisor, err := NewSupervisor(bundle, log, &api)
+ require.NoError(t, err)
+ supervisor.Shutdown()
+}
+
+func testSupervisor_InvalidExecutablePath(t *testing.T) {
+ dir, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600)
+
+ bundle := model.BundleInfoForPath(dir)
+ log := mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: "error",
+ EnableFile: false,
+ })
+ supervisor, err := NewSupervisor(bundle, log, nil)
+ assert.Nil(t, supervisor)
+ assert.Error(t, err)
+}
+
+func testSupervisor_NonExistentExecutablePath(t *testing.T) {
+ dir, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600)
+
+ bundle := model.BundleInfoForPath(dir)
+ log := mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: "error",
+ EnableFile: false,
+ })
+ supervisor, err := NewSupervisor(bundle, log, nil)
+ require.Error(t, err)
+ require.Nil(t, supervisor)
+}
+
+// If plugin development goes really wrong, let's make sure plugin activation won't block forever.
+func testSupervisor_StartTimeout(t *testing.T) {
+ dir, err := ioutil.TempDir("", "")
+ require.NoError(t, err)
+ defer os.RemoveAll(dir)
+
+ backend := filepath.Join(dir, "backend.exe")
+ CompileGo(t, `
+ package main
+
+ func main() {
+ for {
+ }
+ }
+ `, backend)
+
+ ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
+
+ bundle := model.BundleInfoForPath(dir)
+ log := mlog.NewLogger(&mlog.LoggerConfiguration{
+ EnableConsole: true,
+ ConsoleJson: true,
+ ConsoleLevel: "error",
+ EnableFile: false,
+ })
+ supervisor, err := NewSupervisor(bundle, log, nil)
+ require.Error(t, err)
+ require.Nil(t, supervisor)
+}