PLT-7407: Back-end plugin mechanism (#7177)
* begin backend plugin wip * flesh out rpcplugin. everything done except for minor supervisor stubs * done with basic plugin infrastructure * simplify tests * remove unused test lines
Этот коммит содержится в:
62
plugin/rpcplugin/api.go
Обычный файл
62
plugin/rpcplugin/api.go
Обычный файл
@@ -0,0 +1,62 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
)
|
||||
|
||||
type LocalAPI struct {
|
||||
api plugin.API
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func (h *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error {
|
||||
var config interface{}
|
||||
if err := h.api.LoadPluginConfiguration(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = b
|
||||
return nil
|
||||
}
|
||||
|
||||
type RemoteAPI struct {
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalAPI{
|
||||
api: api,
|
||||
muxer: muxer,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
var _ plugin.API = (*RemoteAPI)(nil)
|
||||
|
||||
func (h *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
var config []byte
|
||||
if err := h.client.Call("LocalAPI.LoadPluginConfiguration", struct{}{}, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(config, dest)
|
||||
}
|
||||
|
||||
func (h *RemoteAPI) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI {
|
||||
return &RemoteAPI{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
}
|
||||
}
|
||||
57
plugin/rpcplugin/api_test.go
Обычный файл
57
plugin/rpcplugin/api_test.go
Обычный файл
@@ -0,0 +1,57 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
"github.com/mattermost/platform/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) {
|
||||
var api plugintest.API
|
||||
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)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
77
plugin/rpcplugin/hooks.go
Обычный файл
77
plugin/rpcplugin/hooks.go
Обычный файл
@@ -0,0 +1,77 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
)
|
||||
|
||||
type LocalHooks struct {
|
||||
hooks plugin.Hooks
|
||||
muxer *Muxer
|
||||
remoteAPI *RemoteAPI
|
||||
}
|
||||
|
||||
func (h *LocalHooks) OnActivate(args int64, reply *struct{}) error {
|
||||
stream := h.muxer.Connect(args)
|
||||
if h.remoteAPI != nil {
|
||||
h.remoteAPI.Close()
|
||||
}
|
||||
h.remoteAPI = ConnectAPI(stream, h.muxer)
|
||||
return h.hooks.OnActivate(h.remoteAPI)
|
||||
}
|
||||
|
||||
func (h *LocalHooks) OnDeactivate(args, reply *struct{}) error {
|
||||
err := h.hooks.OnDeactivate()
|
||||
if h.remoteAPI != nil {
|
||||
h.remoteAPI.Close()
|
||||
h.remoteAPI = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type RemoteHooks struct {
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
apiCloser io.Closer
|
||||
}
|
||||
|
||||
func ServeHooks(hooks plugin.Hooks, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalHooks{
|
||||
hooks: hooks,
|
||||
muxer: muxer,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
var _ plugin.Hooks = (*RemoteHooks)(nil)
|
||||
|
||||
func (h *RemoteHooks) OnActivate(api plugin.API) error {
|
||||
id, stream := h.muxer.Serve()
|
||||
if h.apiCloser != nil {
|
||||
h.apiCloser.Close()
|
||||
}
|
||||
h.apiCloser = stream
|
||||
go ServeAPI(api, stream, h.muxer)
|
||||
return h.client.Call("LocalHooks.OnActivate", id, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) OnDeactivate() error {
|
||||
return h.client.Call("LocalHooks.OnDeactivate", struct{}{}, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) Close() error {
|
||||
if h.apiCloser != nil {
|
||||
h.apiCloser.Close()
|
||||
}
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer) *RemoteHooks {
|
||||
return &RemoteHooks{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
}
|
||||
}
|
||||
58
plugin/rpcplugin/hooks_test.go
Обычный файл
58
plugin/rpcplugin/hooks_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
"github.com/mattermost/platform/plugin/plugintest"
|
||||
)
|
||||
|
||||
func testHooksRPC(hooks plugin.Hooks, f func(plugin.Hooks)) {
|
||||
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 := ConnectHooks(c2.Connect(id), c2)
|
||||
defer remote.Close()
|
||||
|
||||
f(remote)
|
||||
}
|
||||
|
||||
func TestHooks(t *testing.T) {
|
||||
var api plugintest.API
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
testHooksRPC(&hooks, func(remote plugin.Hooks) {
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkOnDeactivate(b *testing.B) {
|
||||
var hooks plugintest.Hooks
|
||||
hooks.On("OnDeactivate").Return(nil)
|
||||
|
||||
testHooksRPC(&hooks, func(remote plugin.Hooks) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
remote.OnDeactivate()
|
||||
}
|
||||
b.StopTimer()
|
||||
})
|
||||
}
|
||||
23
plugin/rpcplugin/io.go
Обычный файл
23
plugin/rpcplugin/io.go
Обычный файл
@@ -0,0 +1,23 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type rwc struct {
|
||||
io.ReadCloser
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
func (rwc *rwc) Close() error {
|
||||
rerr := rwc.ReadCloser.Close()
|
||||
werr := rwc.WriteCloser.Close()
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
return werr
|
||||
}
|
||||
|
||||
func NewReadWriteCloser(r io.ReadCloser, w io.WriteCloser) io.ReadWriteCloser {
|
||||
return &rwc{r, w}
|
||||
}
|
||||
28
plugin/rpcplugin/ipc.go
Обычный файл
28
plugin/rpcplugin/ipc.go
Обычный файл
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
61
plugin/rpcplugin/ipc_test.go
Обычный файл
61
plugin/rpcplugin/ipc_test.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIPC(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pingpong := filepath.Join(dir, "pingpong")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mattermost/platform/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())
|
||||
}
|
||||
46
plugin/rpcplugin/main.go
Обычный файл
46
plugin/rpcplugin/main.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
)
|
||||
|
||||
// Makes a set of hooks available via RPC. This function never returns.
|
||||
func Main(hooks plugin.Hooks) {
|
||||
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) (*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), nil
|
||||
}
|
||||
58
plugin/rpcplugin/main_test.go
Обычный файл
58
plugin/rpcplugin/main_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/platform/plugin/plugintest"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
plugin := filepath.Join(dir, "plugin")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/plugin"
|
||||
"github.com/mattermost/platform/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {}
|
||||
|
||||
func (p *MyPlugin) OnActivate(api plugin.API) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, plugin)
|
||||
|
||||
p, ipc, err := NewProcess(context.Background(), plugin)
|
||||
require.NoError(t, err)
|
||||
defer p.Wait()
|
||||
|
||||
muxer := NewMuxer(ipc, false)
|
||||
defer muxer.Close()
|
||||
|
||||
var api plugintest.API
|
||||
|
||||
hooks, err := ConnectMain(muxer)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, hooks.OnActivate(&api))
|
||||
assert.NoError(t, hooks.OnDeactivate())
|
||||
}
|
||||
253
plugin/rpcplugin/muxer.go
Обычный файл
253
plugin/rpcplugin/muxer.go
Обычный файл
@@ -0,0 +1,253 @@
|
||||
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")
|
||||
}
|
||||
buf := make([]byte, 10)
|
||||
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 _, 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
|
||||
}
|
||||
_, 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
|
||||
closeErr error
|
||||
}
|
||||
|
||||
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 {
|
||||
n, err := s.readBuf.Read(p)
|
||||
return n, err
|
||||
}
|
||||
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.isClosed = true
|
||||
s.muxer.rm(s.id)
|
||||
}
|
||||
s.readWake.Signal()
|
||||
return nil
|
||||
}
|
||||
169
plugin/rpcplugin/muxer_test.go
Обычный файл
169
plugin/rpcplugin/muxer_test.go
Обычный файл
@@ -0,0 +1,169 @@
|
||||
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 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])
|
||||
}
|
||||
23
plugin/rpcplugin/process.go
Обычный файл
23
plugin/rpcplugin/process.go
Обычный файл
@@ -0,0 +1,23 @@
|
||||
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()
|
||||
}
|
||||
64
plugin/rpcplugin/process_test.go
Обычный файл
64
plugin/rpcplugin/process_test.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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())
|
||||
}
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ping := filepath.Join(dir, "ping")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mattermost/platform/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())
|
||||
}
|
||||
45
plugin/rpcplugin/process_unix.go
Обычный файл
45
plugin/rpcplugin/process_unix.go
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// +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)
|
||||
}
|
||||
17
plugin/rpcplugin/process_windows.go
Обычный файл
17
plugin/rpcplugin/process_windows.go
Обычный файл
@@ -0,0 +1,17 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
|
||||
// TODO
|
||||
return nil, nil, fmt.Errorf("not yet supported")
|
||||
}
|
||||
|
||||
func inheritedProcessIPC() (*IPC, error) {
|
||||
// TODO
|
||||
return nil, fmt.Errorf("not yet supported")
|
||||
}
|
||||
128
plugin/rpcplugin/supervisor.go
Обычный файл
128
plugin/rpcplugin/supervisor.go
Обычный файл
@@ -0,0 +1,128 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
)
|
||||
|
||||
// Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and
|
||||
// communicates via RPC.
|
||||
//
|
||||
// If the plugin unexpectedly exists, the supervisor will relaunch it after a short delay.
|
||||
type Supervisor struct {
|
||||
executable string
|
||||
hooks atomic.Value
|
||||
done chan bool
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
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() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.done = make(chan bool, 1)
|
||||
start := make(chan error, 1)
|
||||
go s.run(ctx, start)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
defer func() {
|
||||
s.done <- true
|
||||
}()
|
||||
done := ctx.Done()
|
||||
for {
|
||||
s.runPlugin(ctx, start)
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
start = nil
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error) error {
|
||||
p, ipc, err := NewProcess(ctx, s.executable)
|
||||
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)
|
||||
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 *plugin.BundleInfo) (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")
|
||||
}
|
||||
return &Supervisor{
|
||||
executable: filepath.Join(bundle.Path, bundle.Manifest.Backend.Executable),
|
||||
}, nil
|
||||
}
|
||||
130
plugin/rpcplugin/supervisor_test.go
Обычный файл
130
plugin/rpcplugin/supervisor_test.go
Обычный файл
@@ -0,0 +1,130 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
)
|
||||
|
||||
func TestSupervisor(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/plugin"
|
||||
"github.com/mattermost/platform/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {}
|
||||
|
||||
func (p *MyPlugin) OnActivate(api plugin.API) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600)
|
||||
|
||||
bundle := plugin.BundleInfoForPath(dir)
|
||||
supervisor, err := SupervisorProvider(bundle)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, supervisor.Start())
|
||||
require.NoError(t, supervisor.Hooks().OnActivate(nil))
|
||||
require.NoError(t, supervisor.Stop())
|
||||
}
|
||||
|
||||
// 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")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
func main() {
|
||||
for {
|
||||
}
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600)
|
||||
|
||||
bundle := plugin.BundleInfoForPath(dir)
|
||||
supervisor, err := SupervisorProvider(bundle)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, supervisor.Start())
|
||||
}
|
||||
|
||||
// Crashed plugins should be relaunched.
|
||||
func TestSupervisor_PluginCrash(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend")
|
||||
compileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/platform/plugin"
|
||||
"github.com/mattermost/platform/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {}
|
||||
|
||||
func (p *MyPlugin) OnActivate(api plugin.API) error {
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600)
|
||||
|
||||
bundle := plugin.BundleInfoForPath(dir)
|
||||
supervisor, err := SupervisorProvider(bundle)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, supervisor.Start())
|
||||
require.Error(t, supervisor.Hooks().OnActivate(nil))
|
||||
|
||||
recovered := false
|
||||
for i := 0; i < 30; i++ {
|
||||
if supervisor.Hooks().OnDeactivate() == nil {
|
||||
recovered = true
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
assert.True(t, recovered)
|
||||
require.NoError(t, supervisor.Stop())
|
||||
}
|
||||
Ссылка в новой задаче
Block a user