github.com/keys-pub/mattermost-server@v4.10.10+incompatible/plugin/rpcplugin/supervisor.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package rpcplugin 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "path/filepath" 11 "strings" 12 "sync/atomic" 13 "time" 14 15 "github.com/mattermost/mattermost-server/mlog" 16 "github.com/mattermost/mattermost-server/model" 17 "github.com/mattermost/mattermost-server/plugin" 18 ) 19 20 const ( 21 MaxProcessRestarts = 3 22 ) 23 24 // Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and 25 // communicates via RPC. 26 // 27 // If the plugin unexpectedly exits, the supervisor will relaunch it after a short delay, but will 28 // only restart a plugin at most three times. 29 type Supervisor struct { 30 hooks atomic.Value 31 done chan bool 32 cancel context.CancelFunc 33 newProcess func(context.Context) (Process, io.ReadWriteCloser, error) 34 pluginId string 35 } 36 37 var _ plugin.Supervisor = (*Supervisor)(nil) 38 39 // Starts the plugin. This method will block until the plugin is successfully launched for the first 40 // time and will return an error if the plugin cannot be launched at all. 41 func (s *Supervisor) Start(api plugin.API) error { 42 ctx, cancel := context.WithCancel(context.Background()) 43 s.done = make(chan bool, 1) 44 start := make(chan error, 1) 45 go s.run(ctx, start, api) 46 47 select { 48 case <-time.After(time.Second * 3): 49 cancel() 50 <-s.done 51 return fmt.Errorf("timed out waiting for plugin") 52 case err := <-start: 53 s.cancel = cancel 54 return err 55 } 56 } 57 58 // Stops the plugin. 59 func (s *Supervisor) Stop() error { 60 s.cancel() 61 <-s.done 62 return nil 63 } 64 65 // Returns the hooks used to communicate with the plugin. The hooks may change if the plugin is 66 // restarted, so the return value should not be cached. 67 func (s *Supervisor) Hooks() plugin.Hooks { 68 return s.hooks.Load().(plugin.Hooks) 69 } 70 71 func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) { 72 defer func() { 73 s.done <- true 74 }() 75 done := ctx.Done() 76 for i := 0; i <= MaxProcessRestarts; i++ { 77 s.runPlugin(ctx, start, api) 78 select { 79 case <-done: 80 return 81 default: 82 start = nil 83 if i < MaxProcessRestarts { 84 mlog.Debug("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId)) 85 time.Sleep(time.Duration((1 + i*i)) * time.Second) 86 } else { 87 mlog.Debug("Plugin terminated unexpectedly too many times", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts)) 88 } 89 } 90 } 91 } 92 93 func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plugin.API) error { 94 if start == nil { 95 mlog.Debug("Restarting plugin", mlog.String("plugin_id", s.pluginId)) 96 } 97 98 p, ipc, err := s.newProcess(ctx) 99 if err != nil { 100 if start != nil { 101 start <- err 102 } 103 return err 104 } 105 106 muxer := NewMuxer(ipc, false) 107 closeMuxer := make(chan bool, 1) 108 muxerClosed := make(chan error, 1) 109 go func() { 110 select { 111 case <-ctx.Done(): 112 break 113 case <-closeMuxer: 114 break 115 } 116 muxerClosed <- muxer.Close() 117 }() 118 119 hooks, err := ConnectMain(muxer, s.pluginId) 120 if err == nil { 121 err = hooks.OnActivate(api) 122 } 123 124 if err != nil { 125 if start != nil { 126 start <- err 127 } 128 closeMuxer <- true 129 <-muxerClosed 130 p.Wait() 131 return err 132 } 133 134 s.hooks.Store(hooks) 135 136 if start != nil { 137 start <- nil 138 } 139 p.Wait() 140 closeMuxer <- true 141 <-muxerClosed 142 143 return nil 144 } 145 146 func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { 147 return SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (Process, io.ReadWriteCloser, error) { 148 executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) 149 if strings.HasPrefix(executable, "..") { 150 return nil, nil, fmt.Errorf("invalid backend executable") 151 } 152 return NewProcess(ctx, filepath.Join(bundle.Path, executable)) 153 }) 154 } 155 156 func SupervisorWithNewProcessFunc(bundle *model.BundleInfo, newProcess func(context.Context) (Process, io.ReadWriteCloser, error)) (plugin.Supervisor, error) { 157 if bundle.Manifest == nil { 158 return nil, fmt.Errorf("no manifest available") 159 } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" { 160 return nil, fmt.Errorf("no backend executable specified") 161 } 162 executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) 163 if strings.HasPrefix(executable, "..") { 164 return nil, fmt.Errorf("invalid backend executable") 165 } 166 return &Supervisor{pluginId: bundle.Manifest.Id, newProcess: newProcess}, nil 167 }