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 }