1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2// See LICENSE.txt for license information. 3 4package plugin 5 6import ( 7 "fmt" 8 "os/exec" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "sync" 13 "time" 14 15 plugin "github.com/hashicorp/go-plugin" 16 17 "github.com/mattermost/mattermost-server/v6/einterfaces" 18 "github.com/mattermost/mattermost-server/v6/model" 19 "github.com/mattermost/mattermost-server/v6/shared/mlog" 20) 21 22type supervisor struct { 23 lock sync.RWMutex 24 client *plugin.Client 25 hooks Hooks 26 implemented [TotalHooksID]bool 27 pid int 28 hooksClient *hooksRPCClient 29} 30 31func newSupervisor(pluginInfo *model.BundleInfo, apiImpl API, driver Driver, parentLogger *mlog.Logger, metrics einterfaces.MetricsInterface) (retSupervisor *supervisor, retErr error) { 32 sup := supervisor{} 33 defer func() { 34 if retErr != nil { 35 sup.Shutdown() 36 } 37 }() 38 39 wrappedLogger := pluginInfo.WrapLogger(parentLogger) 40 41 hclogAdaptedLogger := &hclogAdapter{ 42 wrappedLogger: wrappedLogger, 43 extrasKey: "wrapped_extras", 44 } 45 46 pluginMap := map[string]plugin.Plugin{ 47 "hooks": &hooksPlugin{ 48 log: wrappedLogger, 49 driverImpl: driver, 50 apiImpl: &apiTimerLayer{pluginInfo.Manifest.Id, apiImpl, metrics}, 51 }, 52 } 53 54 executable := filepath.Clean(filepath.Join( 55 ".", 56 pluginInfo.Manifest.GetExecutableForRuntime(runtime.GOOS, runtime.GOARCH), 57 )) 58 if strings.HasPrefix(executable, "..") { 59 return nil, fmt.Errorf("invalid backend executable") 60 } 61 executable = filepath.Join(pluginInfo.Path, executable) 62 63 cmd := exec.Command(executable) 64 65 sup.client = plugin.NewClient(&plugin.ClientConfig{ 66 HandshakeConfig: handshake, 67 Plugins: pluginMap, 68 Cmd: cmd, 69 SyncStdout: wrappedLogger.With(mlog.String("source", "plugin_stdout")).StdLogWriter(), 70 SyncStderr: wrappedLogger.With(mlog.String("source", "plugin_stderr")).StdLogWriter(), 71 Logger: hclogAdaptedLogger, 72 StartTimeout: time.Second * 3, 73 }) 74 75 rpcClient, err := sup.client.Client() 76 if err != nil { 77 return nil, err 78 } 79 80 sup.pid = cmd.Process.Pid 81 82 raw, err := rpcClient.Dispense("hooks") 83 if err != nil { 84 return nil, err 85 } 86 87 c, ok := raw.(*hooksRPCClient) 88 if ok { 89 sup.hooksClient = c 90 } 91 92 sup.hooks = &hooksTimerLayer{pluginInfo.Manifest.Id, raw.(Hooks), metrics} 93 94 impl, err := sup.hooks.Implemented() 95 if err != nil { 96 return nil, err 97 } 98 for _, hookName := range impl { 99 if hookId, ok := hookNameToId[hookName]; ok { 100 sup.implemented[hookId] = true 101 } 102 } 103 104 return &sup, nil 105} 106 107func (sup *supervisor) Shutdown() { 108 sup.lock.RLock() 109 defer sup.lock.RUnlock() 110 if sup.client != nil { 111 sup.client.Kill() 112 } 113 114 // Wait for API RPC server and DB RPC server to exit. 115 if sup.hooksClient != nil { 116 sup.hooksClient.doneWg.Wait() 117 } 118} 119 120func (sup *supervisor) Hooks() Hooks { 121 sup.lock.RLock() 122 defer sup.lock.RUnlock() 123 return sup.hooks 124} 125 126// PerformHealthCheck checks the plugin through an an RPC ping. 127func (sup *supervisor) PerformHealthCheck() error { 128 // No need for a lock here because Ping is read-locked. 129 if pingErr := sup.Ping(); pingErr != nil { 130 for pingFails := 1; pingFails < HealthCheckPingFailLimit; pingFails++ { 131 pingErr = sup.Ping() 132 if pingErr == nil { 133 break 134 } 135 } 136 if pingErr != nil { 137 return fmt.Errorf("plugin RPC connection is not responding") 138 } 139 } 140 141 return nil 142} 143 144// Ping checks that the RPC connection with the plugin is alive and healthy. 145func (sup *supervisor) Ping() error { 146 sup.lock.RLock() 147 defer sup.lock.RUnlock() 148 client, err := sup.client.Client() 149 if err != nil { 150 return err 151 } 152 153 return client.Ping() 154} 155 156func (sup *supervisor) Implements(hookId int) bool { 157 sup.lock.RLock() 158 defer sup.lock.RUnlock() 159 return sup.implemented[hookId] 160} 161