mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-07 08:49:37 +08:00
feat: support creating and calling custom functions with hashicorp/go-plugin
This commit is contained in:
55
plugin-gosdk/build.go
Normal file
55
plugin-gosdk/build.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// Here is a real implementation of funcCaller
|
||||
type functionPlugin struct {
|
||||
logger hclog.Logger
|
||||
functions map[string]func(args ...interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
func (p *functionPlugin) GetNames() ([]string, error) {
|
||||
var names []string
|
||||
for name := range p.functions {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (p *functionPlugin) Call(funcName string, args ...interface{}) (interface{}, error) {
|
||||
p.logger.Info("Call function", "funcName", funcName, "args", args)
|
||||
|
||||
f, ok := p.functions[funcName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("function %s not found", funcName)
|
||||
}
|
||||
|
||||
return f(args...)
|
||||
}
|
||||
|
||||
var functions = make(map[string]func(args ...interface{}) (interface{}, error))
|
||||
|
||||
func Register(funcName string, fn func(args ...interface{}) (interface{}, error)) {
|
||||
functions[funcName] = fn
|
||||
}
|
||||
|
||||
func Serve() {
|
||||
funcPlugin := &functionPlugin{
|
||||
logger: logger,
|
||||
functions: functions,
|
||||
}
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
Name: &hashicorpPlugin{Impl: funcPlugin},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
})
|
||||
}
|
||||
42
plugin-gosdk/host_test.go
Normal file
42
plugin-gosdk/host_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
fmt.Println("[TestMain] build go plugin")
|
||||
cmd := exec.Command("go", "build", "-o=debugtalk", "plugin/debugtalk.go")
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestInitHashicorpPlugin(t *testing.T) {
|
||||
f, err := Init("./debugtalk")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Quit()
|
||||
|
||||
v, err := f.Call("sum_int", 1, 2, 3, 4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, 10, v) {
|
||||
t.Fail()
|
||||
}
|
||||
v, err = f.Call("concatenate_string", "a", "b", "c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "abc", v) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
105
plugin-gosdk/interface.go
Normal file
105
plugin-gosdk/interface.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(new(funcData))
|
||||
}
|
||||
|
||||
// funcData is used to transfer between plugin and host via RPC.
|
||||
type funcData struct {
|
||||
Name string // function name
|
||||
Args []interface{} // function arguments
|
||||
}
|
||||
|
||||
// FuncCaller is the interface that we're exposing as a plugin.
|
||||
type FuncCaller interface {
|
||||
GetNames() ([]string, error) // get all plugin function names list
|
||||
Call(funcName string, args ...interface{}) (interface{}, error) // call plugin function
|
||||
}
|
||||
|
||||
// functionRPC runs on the host side.
|
||||
type functionRPC struct {
|
||||
client *rpc.Client
|
||||
}
|
||||
|
||||
func (g *functionRPC) GetNames() ([]string, error) {
|
||||
var resp []string
|
||||
err := g.client.Call("Plugin.GetNames", new(interface{}), &resp)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("rpc call GetNames() failed")
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// host -> plugin
|
||||
func (g *functionRPC) Call(funcName string, funcArgs ...interface{}) (interface{}, error) {
|
||||
log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via RPC")
|
||||
f := funcData{
|
||||
Name: funcName,
|
||||
Args: funcArgs,
|
||||
}
|
||||
|
||||
var args interface{} = f
|
||||
var resp interface{}
|
||||
err := g.client.Call("Plugin.Call", &args, &resp)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("funcName", funcName).Interface("funcArgs", funcArgs).
|
||||
Msg("rpc call Call() failed")
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// functionRPCServer runs on the plugin side, executing the user custom function.
|
||||
type functionRPCServer struct {
|
||||
Impl FuncCaller
|
||||
}
|
||||
|
||||
// plugin execution
|
||||
func (s *functionRPCServer) GetNames(args interface{}, resp *[]string) error {
|
||||
log.Info().Interface("args", args).Msg("GetNames called on plugin side")
|
||||
var err error
|
||||
*resp, err = s.Impl.GetNames()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("GetNames execution failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// plugin execution
|
||||
func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error {
|
||||
log.Info().Interface("args", args).Msg("function called on plugin side")
|
||||
f := args.(*funcData)
|
||||
var err error
|
||||
*resp, err = s.Impl.Call(f.Name, f.Args...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Interface("args", args).
|
||||
Msg("function execution failed")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashicorpPlugin implements hashicorp's plugin.Plugin.
|
||||
type hashicorpPlugin struct {
|
||||
Impl FuncCaller
|
||||
}
|
||||
|
||||
func (p *hashicorpPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
|
||||
return &functionRPCServer{Impl: p.Impl}, nil
|
||||
}
|
||||
|
||||
func (hashicorpPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
|
||||
return &functionRPC{client: c}, nil
|
||||
}
|
||||
63
plugin-gosdk/main.go
Normal file
63
plugin-gosdk/main.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
const Name = "debugtalk"
|
||||
|
||||
// handshakeConfigs are used to just do a basic handshake between
|
||||
// a plugin and host. If the handshake fails, a user friendly error is shown.
|
||||
// This prevents users from executing bad plugins or executing a plugin
|
||||
// directory. It is a UX feature, not a security feature.
|
||||
var handshakeConfig = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 1,
|
||||
MagicCookieKey: "HttpRunnerPlus",
|
||||
MagicCookieValue: Name,
|
||||
}
|
||||
|
||||
// Create an hclog.Logger
|
||||
var logger = hclog.New(&hclog.LoggerOptions{
|
||||
Name: Name,
|
||||
Output: os.Stdout,
|
||||
Level: hclog.Debug,
|
||||
})
|
||||
|
||||
var client *plugin.Client
|
||||
|
||||
func Init(path string) (FuncCaller, error) {
|
||||
// launch the plugin process
|
||||
client = plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: map[string]plugin.Plugin{
|
||||
Name: &hashicorpPlugin{},
|
||||
},
|
||||
Cmd: exec.Command(path),
|
||||
Logger: logger,
|
||||
})
|
||||
|
||||
// Connect via RPC
|
||||
rpcClient, err := client.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Request the plugin
|
||||
raw, err := rpcClient.Dispense(Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We should have a Function now! This feels like a normal interface
|
||||
// implementation but is in fact over an RPC connection.
|
||||
function := raw.(FuncCaller)
|
||||
return function, nil
|
||||
}
|
||||
|
||||
func Quit() {
|
||||
client.Kill()
|
||||
}
|
||||
38
plugin-gosdk/plugin/debugtalk.go
Normal file
38
plugin-gosdk/plugin/debugtalk.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
plugin "github.com/httprunner/hrp/plugin-gosdk"
|
||||
)
|
||||
|
||||
func SumInt(args ...interface{}) (interface{}, error) {
|
||||
var sum int
|
||||
for _, arg := range args {
|
||||
v, ok := arg.(int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type: %T", arg)
|
||||
}
|
||||
sum += v
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func ConcatenateString(args ...interface{}) (interface{}, error) {
|
||||
var result string
|
||||
for _, arg := range args {
|
||||
v, ok := arg.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type: %T", arg)
|
||||
}
|
||||
result += v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// register functions and build to plugin binary
|
||||
func main() {
|
||||
plugin.Register("sum_int", SumInt)
|
||||
plugin.Register("concatenate_string", ConcatenateString)
|
||||
plugin.Serve()
|
||||
}
|
||||
Reference in New Issue
Block a user