github.com/qichengzx/mattermost-server@v4.5.1-0.20180604164826-2c75247c97d0+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  
   178  	var supervisorWaitErr error
   179  	supervisorWaitDone := make(chan bool, 1)
   180  	go func() {
   181  		supervisorWaitErr = supervisor.Wait()
   182  		close(supervisorWaitDone)
   183  	}()
   184  
   185  	require.NoError(t, supervisor.Start(&api))
   186  
   187  	failed := false
   188  	recovered := false
   189  	for i := 0; i < 30; i++ {
   190  		if supervisor.Hooks().OnDeactivate() == nil {
   191  			require.True(t, failed)
   192  			recovered = true
   193  			break
   194  		} else {
   195  			failed = true
   196  		}
   197  		time.Sleep(time.Millisecond * 100)
   198  	}
   199  	assert.True(t, recovered)
   200  
   201  	select {
   202  	case <-supervisorWaitDone:
   203  		require.Fail(t, "supervisor.Wait() unexpectedly returned")
   204  	case <-time.After(500 * time.Millisecond):
   205  	}
   206  
   207  	require.NoError(t, supervisor.Stop())
   208  
   209  	select {
   210  	case <-supervisorWaitDone:
   211  		require.Nil(t, supervisorWaitErr)
   212  	case <-time.After(5000 * time.Millisecond):
   213  		require.Fail(t, "supervisor.Wait() failed to return")
   214  	}
   215  }
   216  
   217  // Crashed plugins should be relaunched at most three times.
   218  func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFunc) {
   219  	dir, err := ioutil.TempDir("", "")
   220  	require.NoError(t, err)
   221  	defer os.RemoveAll(dir)
   222  
   223  	backend := filepath.Join(dir, "backend.exe")
   224  	CompileGo(t, `
   225  		package main
   226  
   227  		import (
   228  			"net/http"
   229  			"os"
   230  
   231  			"github.com/mattermost/mattermost-server/plugin/rpcplugin"
   232  		)
   233  
   234  		type MyPlugin struct {
   235  			crashing bool
   236  		}
   237  
   238  		func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   239  			if r.Method == http.MethodPost {
   240  				p.crashing = true
   241  				go func() {
   242  					os.Exit(1)
   243  				}()
   244  			}
   245  
   246  			if p.crashing {
   247  				w.WriteHeader(http.StatusInternalServerError)
   248  			} else {
   249  				w.WriteHeader(http.StatusOK)
   250  			}
   251  		}
   252  
   253  		func main() {
   254  			rpcplugin.Main(&MyPlugin{})
   255  		}
   256  	`, backend)
   257  
   258  	ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
   259  
   260  	var api plugintest.API
   261  	bundle := model.BundleInfoForPath(dir)
   262  	supervisor, err := sp(bundle)
   263  	require.NoError(t, err)
   264  
   265  	var supervisorWaitErr error
   266  	supervisorWaitDone := make(chan bool, 1)
   267  	go func() {
   268  		supervisorWaitErr = supervisor.Wait()
   269  		close(supervisorWaitDone)
   270  	}()
   271  
   272  	require.NoError(t, supervisor.Start(&api))
   273  
   274  	for attempt := 1; attempt <= 4; attempt++ {
   275  		// Verify that the plugin is operational
   276  		response := httptest.NewRecorder()
   277  		supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
   278  		require.Equal(t, http.StatusOK, response.Result().StatusCode)
   279  
   280  		// Crash the plugin
   281  		supervisor.Hooks().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/plugins/id", nil))
   282  
   283  		// Wait for it to potentially recover
   284  		recovered := false
   285  		for i := 0; i < 125; i++ {
   286  			response := httptest.NewRecorder()
   287  			supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
   288  			if response.Result().StatusCode == http.StatusOK {
   289  				recovered = true
   290  				break
   291  			}
   292  
   293  			time.Sleep(time.Millisecond * 100)
   294  		}
   295  
   296  		if attempt < 4 {
   297  			require.Nil(t, supervisorWaitErr)
   298  			require.True(t, recovered, "failed to recover after attempt %d", attempt)
   299  		} else {
   300  			require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt)
   301  		}
   302  	}
   303  
   304  	select {
   305  	case <-supervisorWaitDone:
   306  		require.NotNil(t, supervisorWaitErr)
   307  	case <-time.After(500 * time.Millisecond):
   308  		require.Fail(t, "supervisor.Wait() failed to return after plugin crashed")
   309  	}
   310  
   311  	require.NoError(t, supervisor.Stop())
   312  }