github.com/drellem2/pogo@v0.0.0-20240503070746-2c2b76da329a/internal/driver/driver.go (about) 1 //////////////////////////////////////////////////////////////////////////////// 2 ////////// Plugin driver ////////// 3 //////////////////////////////////////////////////////////////////////////////// 4 5 package driver 6 7 import ( 8 "crypto/sha256" 9 "encoding/gob" 10 "errors" 11 "fmt" 12 "io" 13 "log" 14 "os" 15 "os/exec" 16 "path/filepath" 17 18 pogoPlugin "github.com/drellem2/pogo/pkg/plugin" 19 hclog "github.com/hashicorp/go-hclog" 20 "github.com/hashicorp/go-plugin" 21 ) 22 23 var logger = hclog.New(&hclog.LoggerOptions{ 24 Name: "driver", 25 Output: os.Stdout, 26 Level: hclog.Debug, 27 }) 28 29 // handshakeConfigs are used to just do a basic handshake between 30 // a plugin and host. If the handshake fails, a user friendly error is shown. 31 // This prevents users from executing bad plugins or executing a plugin 32 // directory. It is a UX feature, not a security feature. 33 var handshakeConfig = plugin.HandshakeConfig{ 34 ProtocolVersion: 2, 35 MagicCookieKey: "SEARCH_PLUGIN", 36 MagicCookieValue: "93f6bc9f97c03ed00fa85c904aca15a92752e549", 37 } 38 39 // pluginMap is the map of plugins we can dispense. 40 var pluginMap = map[string]plugin.Plugin{ 41 "basicSearch": &pogoPlugin.PogoPlugin{}, 42 } 43 44 var clients map[string]*plugin.Client 45 var Interfaces map[string]*pogoPlugin.IPogoPlugin 46 47 type PluginManager struct { 48 } 49 50 type PluginInfoReq struct { 51 Path string `json:"path"` 52 } 53 54 func GetPluginManager() *PluginManager { 55 return &PluginManager{} 56 } 57 58 func GetPluginPaths() []string { 59 keys := make([]string, len(Interfaces)) 60 i := 0 61 for k := range Interfaces { 62 keys[i] = k 63 i++ 64 } 65 return keys 66 } 67 68 func (g *PluginManager) Info() *pogoPlugin.PluginInfoRes { 69 return &pogoPlugin.PluginInfoRes{Version: ""} 70 } 71 72 func GetPluginClient(path string) *plugin.Client { 73 return clients[path] 74 } 75 76 func GetPlugin(path string) *pogoPlugin.IPogoPlugin { 77 checkAlive(path) 78 return Interfaces[path] 79 } 80 81 func checkAlive(path string) { 82 if Interfaces[path] == nil { 83 startPlugin(path) 84 return 85 } 86 87 // Built-in plugin 88 if clients[path] == nil { 89 return 90 } 91 92 // Check if plugin is alive 93 if clients[path].Exited() { 94 // Plugin is dead, restart 95 startPlugin(path) 96 } 97 } 98 99 func (g *PluginManager) ProcessProject(req *pogoPlugin.IProcessProjectReq) error { 100 var err error 101 hasErr := false 102 for path, _ := range Interfaces { 103 checkAlive(path) 104 } 105 for _, raw := range Interfaces { 106 func() { 107 defer func() { 108 if r := recover(); r != nil { 109 fmt.Printf("Caught error: %v", r) 110 hasErr = true 111 } 112 }() 113 basicSearch := pogoPlugin.IPogoPlugin(*raw) 114 err = basicSearch.ProcessProject(req) 115 if err != nil { 116 fmt.Printf("Caught error calling ProcessProject(): %v", err) 117 } 118 }() 119 } 120 if hasErr { 121 return errors.New("Error calling ProcessProject") 122 } 123 return nil 124 } 125 126 func Init() { 127 gob.Register(pogoPlugin.ProcessProjectReq{}) 128 clients = make(map[string]*plugin.Client) 129 Interfaces = make(map[string]*pogoPlugin.IPogoPlugin) 130 131 relativePluginPath := os.Getenv("POGO_PLUGIN_PATH") 132 pluginPath, err := filepath.Abs(relativePluginPath) 133 if err != nil { 134 fmt.Printf("Error getting absolute path for %s: %v", relativePluginPath, err) 135 return 136 } 137 138 // Test if pluginPath is empty string or whitespace 139 140 if pluginPath == "" { 141 fmt.Printf("POGO_PLUGIN_PATH not set, using current directory\n") 142 pluginPath, _ = os.Getwd() 143 } 144 145 paths, err := plugin.Discover("pogo*", pluginPath) 146 if err != nil { 147 fmt.Printf("Error discovering plugins: %v", err) 148 return 149 } 150 fmt.Printf("Discovered %d plugins in dir %s: %v\n", len(paths), pluginPath, paths) 151 for _, path := range paths { 152 // Try Windows workaround 153 // Replace double backslash with single backslash 154 // If windows, replace single backslash with double backslash 155 // if os.PathSeparator == '\\' { 156 // path = strings.Replace(path, "\\\\", "\\", -1) 157 // path = strings.Replace(path, "\\", "/", -1) 158 // } 159 func() { 160 defer func() { 161 if r := recover(); r != nil { 162 fmt.Printf("Caught error during plugin creation: %v", r) 163 } 164 }() 165 startPlugin(path) 166 }() 167 } 168 for name, plugin := range builtinRegistry { 169 if Interfaces[name] != nil { 170 logger.Debug("Found runtime copy of plugin, skipping builtin", "name", name) 171 } else { 172 Interfaces[name] = plugin 173 } 174 } 175 } 176 177 func startPlugin(path string) { 178 // Create an hclog.Logger 179 logger := hclog.New(&hclog.LoggerOptions{ 180 Name: path, 181 Output: os.Stdout, 182 Level: hclog.Debug, 183 }) 184 // Start plugins 185 file, err := os.Open(path) 186 if err != nil { 187 log.Fatal(err) 188 } 189 defer file.Close() 190 191 hash := sha256.New() 192 193 _, err = io.Copy(hash, file) 194 if err != nil { 195 log.Fatal(err) 196 } 197 198 sum := hash.Sum(nil) 199 200 secureConfig := &plugin.SecureConfig{ 201 Checksum: sum, 202 Hash: sha256.New(), 203 } 204 205 // We're a host! Start by launching the plugin process. 206 client := plugin.NewClient(&plugin.ClientConfig{ 207 HandshakeConfig: handshakeConfig, 208 Plugins: pluginMap, 209 Cmd: exec.Command(path), 210 Logger: logger, 211 SecureConfig: secureConfig, 212 }) 213 clients[path] = client 214 215 // Connect via RPC 216 rpcClient, err := client.Client() 217 if err != nil { 218 log.Fatal(err) 219 } 220 221 // Request the plugin 222 raw, err := rpcClient.Dispense("basicSearch") 223 if err != nil { 224 log.Fatal(err) 225 } 226 praw := raw.(pogoPlugin.IPogoPlugin) 227 Interfaces[path] = &praw 228 } 229 230 // Clean up, kills all plugins 231 func Kill() { 232 for _, client := range clients { 233 func() { 234 defer func() { 235 if r := recover(); r != nil { 236 log.Printf("Caught error during plugin termination: %v", r) 237 } 238 }() 239 if client != nil { 240 client.Kill() 241 } 242 }() 243 } 244 }