180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-argmapper"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-plugin"
|
|
|
|
"github.com/hashicorp/vagrant-plugin-sdk/component"
|
|
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/pluginclient"
|
|
)
|
|
|
|
// exePath contains the value of os.Executable. We cache the value because
|
|
// we use it a lot and subsequent calls perform syscalls.
|
|
var exePath string
|
|
|
|
func init() {
|
|
var err error
|
|
exePath, err = os.Executable()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Factory returns the factory function for a plugin that is already
|
|
// represented by an *exec.Cmd. This returns an *Instance and NOT the component
|
|
// interface value directly. This instance lets you more carefully manage the
|
|
// lifecycle of the plugin as well as get additional information about the
|
|
// plugin.
|
|
func Factory(cmd *exec.Cmd, typ component.Type) interface{} {
|
|
return func(log hclog.Logger) (interface{}, error) {
|
|
// We have to copy the command because go-plugin will set some
|
|
// fields on it.
|
|
cmdCopy := *cmd
|
|
|
|
config := pluginclient.ClientConfig(log)
|
|
config.Cmd = &cmdCopy
|
|
config.Logger = log
|
|
|
|
// Log that we're going to launch this
|
|
log.Info("launching plugin",
|
|
"type", typ,
|
|
"path", cmd.Path,
|
|
"args", cmd.Args)
|
|
|
|
// Connect to the plugin
|
|
client := plugin.NewClient(config)
|
|
rpcClient, err := client.Client()
|
|
if err != nil {
|
|
log.Error("error creating plugin client",
|
|
"error", err)
|
|
client.Kill()
|
|
return nil, err
|
|
}
|
|
|
|
// Request the plugin. We don't request the mapper type because
|
|
// we handle that below.
|
|
var raw interface{}
|
|
if typ != component.MapperType {
|
|
raw, err = rpcClient.Dispense(strings.ToLower(typ.String()))
|
|
if err != nil {
|
|
log.Error("error requesting plugin",
|
|
"type", typ,
|
|
"error", err)
|
|
|
|
client.Kill()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
b, ok := raw.(hasGRPCBroker)
|
|
if !ok {
|
|
log.Error("cannot extract grpc broker from plugin client")
|
|
client.Kill()
|
|
return nil, fmt.Errorf("unable to extract broker from plugin client")
|
|
}
|
|
|
|
// Request the mappers
|
|
mappers, err := pluginclient.Mappers(client)
|
|
if err != nil {
|
|
log.Error("error requesting plugin mappers",
|
|
"error", err)
|
|
client.Kill()
|
|
return nil, err
|
|
}
|
|
|
|
log.Debug("plugin successfully launched and connected")
|
|
return &Instance{
|
|
Component: raw,
|
|
Broker: b.GRPCBroker(),
|
|
Mappers: mappers,
|
|
Close: func() { client.Kill() },
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// BuiltinFactory creates a factory for a built-in plugin type.
|
|
func BuiltinFactory(name string, typ component.Type) interface{} {
|
|
cmd := exec.Command(exePath, "plugin-run", name)
|
|
|
|
// For non-windows systems, we attach stdout/stderr as extra fds
|
|
// so that we can get direct access to the TTY if possible for output.
|
|
if runtime.GOOS != "windows" {
|
|
cmd.ExtraFiles = []*os.File{os.Stdout, os.Stderr}
|
|
}
|
|
|
|
return Factory(cmd, typ)
|
|
}
|
|
|
|
type PluginMetadata interface {
|
|
SetRequestMetadata(k, v string)
|
|
}
|
|
|
|
func BuiltinRubyFactory(rubyClient plugin.ClientProtocol, name string, typ component.Type) interface{} {
|
|
return func(log hclog.Logger) (interface{}, error) {
|
|
raw, err := rubyClient.Dispense(strings.ToLower(typ.String()))
|
|
if err != nil {
|
|
log.Error("error requesting the ruby plugin",
|
|
"type", typ,
|
|
"name", name,
|
|
"error", err)
|
|
|
|
return nil, err
|
|
}
|
|
|
|
setter, ok := raw.(PluginMetadata)
|
|
if !ok {
|
|
return nil, fmt.Errorf("ruby runtime plugin does not support name setting (%s - %s)",
|
|
typ.String(), name)
|
|
}
|
|
setter.SetRequestMetadata("plugin_name", name)
|
|
log.Trace("set plugin name on legacy ruby plugin",
|
|
"type", typ.String(),
|
|
"name", name,
|
|
"plugin", hclog.Fmt("%T", raw))
|
|
|
|
b, ok := raw.(hasGRPCBroker)
|
|
if !ok {
|
|
log.Error("cannot extract grpc broker from plugin client",
|
|
"type", typ,
|
|
"name", name)
|
|
|
|
return nil, fmt.Errorf("unable to extract broker from builtin ruby plugin client")
|
|
}
|
|
|
|
return &Instance{
|
|
Component: raw,
|
|
Broker: b.GRPCBroker(),
|
|
Mappers: nil,
|
|
Close: func() {},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Instance is the result generated by the factory. This lets us pack
|
|
// a bit more information into plugin-launched components.
|
|
type Instance struct {
|
|
// Component is the dispensed component
|
|
Component interface{}
|
|
|
|
// Mappers is the list of mappers that this plugin is providing.
|
|
Mappers []*argmapper.Func
|
|
|
|
// The GRPCBroker attached to this plugin
|
|
Broker *plugin.GRPCBroker
|
|
|
|
// Closer is a function that should be called to clean up resources
|
|
// associated with this plugin.
|
|
Close func()
|
|
}
|
|
|
|
type hasGRPCBroker interface {
|
|
GRPCBroker() *plugin.GRPCBroker
|
|
}
|