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  }