Chris Roberts bba557c29d
Close the client to shutdown the plugin and prevent hang.
This also updates the Ruby runtime usage to pass the ClientProtocol
    instead of the full client since after init we only need it for
    dispensing plugins.
2022-04-25 12:23:59 -05:00

138 lines
3.7 KiB
Go

package plugin
import (
"errors"
"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", "err", 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, "err", err)
client.Kill()
return nil, err
}
}
// Request the mappers
mappers, err := pluginclient.Mappers(client)
if err != nil {
log.Error("error requesting plugin mappers", "err", err)
client.Kill()
return nil, err
}
log.Debug("plugin successfully launched and connected")
return &Instance{
Component: raw,
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, "err", err)
return nil, err
}
setter, ok := raw.(PluginMetadata)
if !ok {
return nil, errors.New("ruby runtime plugin does not support name setting")
}
setter.SetRequestMetadata("plugin_name", name)
return &Instance{
Component: raw,
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
// Closer is a function that should be called to clean up resources
// associated with this plugin.
Close func()
}