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  }