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 }