github.com/keys-pub/mattermost-server@v4.10.10+incompatible/plugin/rpcplugin/rpcplugintest/supervisor.go (about)

     1  // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package rpcplugintest
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path/filepath"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/mock"
    19  	"github.com/stretchr/testify/require"
    20  
    21  	"github.com/mattermost/mattermost-server/model"
    22  	"github.com/mattermost/mattermost-server/plugin"
    23  	"github.com/mattermost/mattermost-server/plugin/plugintest"
    24  )
    25  
    26  type SupervisorProviderFunc = func(*model.BundleInfo) (plugin.Supervisor, error)
    27  
    28  func TestSupervisorProvider(t *testing.T, sp SupervisorProviderFunc) {
    29  	for name, f := range map[string]func(*testing.T, SupervisorProviderFunc){
    30  		"Supervisor":                           testSupervisor,
    31  		"Supervisor_InvalidExecutablePath":     testSupervisor_InvalidExecutablePath,
    32  		"Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath,
    33  		"Supervisor_StartTimeout":              testSupervisor_StartTimeout,
    34  		"Supervisor_PluginCrash":               testSupervisor_PluginCrash,
    35  		"Supervisor_PluginRepeatedlyCrash":     testSupervisor_PluginRepeatedlyCrash,
    36  	} {
    37  		t.Run(name, func(t *testing.T) { f(t, sp) })
    38  	}
    39  }
    40  
    41  func testSupervisor(t *testing.T, sp SupervisorProviderFunc) {
    42  	dir, err := ioutil.TempDir("", "")
    43  	require.NoError(t, err)
    44  	defer os.RemoveAll(dir)
    45  
    46  	backend := filepath.Join(dir, "backend.exe")
    47  	CompileGo(t, `
    48  		package main
    49  
    50  		import (
    51  			"github.com/mattermost/mattermost-server/plugin/rpcplugin"
    52  		)
    53  
    54  		type MyPlugin struct {}
    55  
    56  		func main() {
    57  			rpcplugin.Main(&MyPlugin{})
    58  		}
    59  	`, backend)
    60  
    61  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
    62  
    63  	bundle := model.BundleInfoForPath(dir)
    64  	supervisor, err := sp(bundle)
    65  	require.NoError(t, err)
    66  	require.NoError(t, supervisor.Start(nil))
    67  	require.NoError(t, supervisor.Stop())
    68  }
    69  
    70  func testSupervisor_InvalidExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
    71  	dir, err := ioutil.TempDir("", "")
    72  	require.NoError(t, err)
    73  	defer os.RemoveAll(dir)
    74  
    75  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600)
    76  
    77  	bundle := model.BundleInfoForPath(dir)
    78  	supervisor, err := sp(bundle)
    79  	assert.Nil(t, supervisor)
    80  	assert.Error(t, err)
    81  }
    82  
    83  func testSupervisor_NonExistentExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
    84  	dir, err := ioutil.TempDir("", "")
    85  	require.NoError(t, err)
    86  	defer os.RemoveAll(dir)
    87  
    88  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600)
    89  
    90  	bundle := model.BundleInfoForPath(dir)
    91  	supervisor, err := sp(bundle)
    92  	require.NotNil(t, supervisor)
    93  	require.NoError(t, err)
    94  
    95  	require.Error(t, supervisor.Start(nil))
    96  }
    97  
    98  // If plugin development goes really wrong, let's make sure plugin activation won't block forever.
    99  func testSupervisor_StartTimeout(t *testing.T, sp SupervisorProviderFunc) {
   100  	dir, err := ioutil.TempDir("", "")
   101  	require.NoError(t, err)
   102  	defer os.RemoveAll(dir)
   103  
   104  	backend := filepath.Join(dir, "backend.exe")
   105  	CompileGo(t, `
   106  		package main
   107  
   108  		func main() {
   109  			for {
   110  			}
   111  		}
   112  	`, backend)
   113  
   114  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
   115  
   116  	bundle := model.BundleInfoForPath(dir)
   117  	supervisor, err := sp(bundle)
   118  	require.NoError(t, err)
   119  	require.Error(t, supervisor.Start(nil))
   120  }
   121  
   122  // Crashed plugins should be relaunched.
   123  func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) {
   124  	dir, err := ioutil.TempDir("", "")
   125  	require.NoError(t, err)
   126  	defer os.RemoveAll(dir)
   127  
   128  	backend := filepath.Join(dir, "backend.exe")
   129  	CompileGo(t, `
   130  		package main
   131  
   132  		import (
   133  			"os"
   134  
   135  			"github.com/mattermost/mattermost-server/plugin"
   136  			"github.com/mattermost/mattermost-server/plugin/rpcplugin"
   137  		)
   138  
   139  		type Configuration struct {
   140  			ShouldExit bool
   141  		}
   142  
   143  		type MyPlugin struct {
   144  			config Configuration
   145  		}
   146  
   147  		func (p *MyPlugin) OnActivate(api plugin.API) error {
   148  			api.LoadPluginConfiguration(&p.config)
   149  			return nil
   150  		}
   151  
   152  		func (p *MyPlugin) OnDeactivate() error {
   153  			if p.config.ShouldExit {
   154  				os.Exit(1)
   155  			}
   156  			return nil
   157  		}
   158  
   159  		func main() {
   160  			rpcplugin.Main(&MyPlugin{})
   161  		}
   162  	`, backend)
   163  
   164  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
   165  
   166  	var api plugintest.API
   167  	shouldExit := true
   168  	api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error {
   169  		err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest)
   170  		shouldExit = false
   171  		return err
   172  	})
   173  
   174  	bundle := model.BundleInfoForPath(dir)
   175  	supervisor, err := sp(bundle)
   176  	require.NoError(t, err)
   177  	require.NoError(t, supervisor.Start(&api))
   178  
   179  	failed := false
   180  	recovered := false
   181  	for i := 0; i < 30; i++ {
   182  		if supervisor.Hooks().OnDeactivate() == nil {
   183  			require.True(t, failed)
   184  			recovered = true
   185  			break
   186  		} else {
   187  			failed = true
   188  		}
   189  		time.Sleep(time.Millisecond * 100)
   190  	}
   191  	assert.True(t, recovered)
   192  	require.NoError(t, supervisor.Stop())
   193  }
   194  
   195  // Crashed plugins should be relaunched at most three times.
   196  func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFunc) {
   197  	dir, err := ioutil.TempDir("", "")
   198  	require.NoError(t, err)
   199  	defer os.RemoveAll(dir)
   200  
   201  	backend := filepath.Join(dir, "backend.exe")
   202  	CompileGo(t, `
   203  		package main
   204  
   205  		import (
   206  			"net/http"
   207  			"os"
   208  
   209  			"github.com/mattermost/mattermost-server/plugin/rpcplugin"
   210  		)
   211  
   212  		type MyPlugin struct {
   213  			crashing bool
   214  		}
   215  
   216  		func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   217  			if r.Method == http.MethodPost {
   218  				p.crashing = true
   219  				go func() {
   220  					os.Exit(1)
   221  				}()
   222  			}
   223  
   224  			if p.crashing {
   225  				w.WriteHeader(http.StatusInternalServerError)
   226  			} else {
   227  				w.WriteHeader(http.StatusOK)
   228  			}
   229  		}
   230  
   231  		func main() {
   232  			rpcplugin.Main(&MyPlugin{})
   233  		}
   234  	`, backend)
   235  
   236  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
   237  
   238  	var api plugintest.API
   239  	bundle := model.BundleInfoForPath(dir)
   240  	supervisor, err := sp(bundle)
   241  	require.NoError(t, err)
   242  	require.NoError(t, supervisor.Start(&api))
   243  
   244  	for attempt := 1; attempt <= 4; attempt++ {
   245  		// Verify that the plugin is operational
   246  		response := httptest.NewRecorder()
   247  		supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
   248  		require.Equal(t, http.StatusOK, response.Result().StatusCode)
   249  
   250  		// Crash the plugin
   251  		supervisor.Hooks().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/plugins/id", nil))
   252  
   253  		// Wait for it to potentially recover
   254  		recovered := false
   255  		for i := 0; i < 125; i++ {
   256  			response := httptest.NewRecorder()
   257  			supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
   258  			if response.Result().StatusCode == http.StatusOK {
   259  				recovered = true
   260  				break
   261  			}
   262  
   263  			time.Sleep(time.Millisecond * 100)
   264  		}
   265  
   266  		if attempt < 4 {
   267  			require.True(t, recovered, "failed to recover after attempt %d", attempt)
   268  		} else {
   269  			require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt)
   270  		}
   271  	}
   272  	require.NoError(t, supervisor.Stop())
   273  }