From b8d6192a34cae9f2963d0035cceb1287d5526c5e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 14 Jan 2022 16:37:36 +0800 Subject: [PATCH] refactor: plugin functions can be defined in any format --- plugin-gosdk/build.go | 55 ------------- plugin-gosdk/host_test.go | 42 ---------- plugin-gosdk/main.go | 63 -------------- plugin-gosdk/plugin/debugtalk.go | 38 --------- plugin.go | 63 +++----------- plugin/examples/debugtalk.go | 65 +++++++++++++++ plugin/examples/plugin_test.go | 87 ++++++++++++++++++++ plugin/host/host.go | 50 +++++++++++ plugin/plugin.go | 67 +++++++++++++++ plugin/shared/config.go | 15 ++++ {plugin-gosdk => plugin/shared}/interface.go | 14 ++-- plugin/shared/utils.go | 50 +++++++++++ 12 files changed, 350 insertions(+), 259 deletions(-) delete mode 100644 plugin-gosdk/build.go delete mode 100644 plugin-gosdk/host_test.go delete mode 100644 plugin-gosdk/main.go delete mode 100644 plugin-gosdk/plugin/debugtalk.go create mode 100644 plugin/examples/debugtalk.go create mode 100644 plugin/examples/plugin_test.go create mode 100644 plugin/host/host.go create mode 100644 plugin/plugin.go create mode 100644 plugin/shared/config.go rename {plugin-gosdk => plugin/shared}/interface.go (88%) create mode 100644 plugin/shared/utils.go diff --git a/plugin-gosdk/build.go b/plugin-gosdk/build.go deleted file mode 100644 index 53be86d9..00000000 --- a/plugin-gosdk/build.go +++ /dev/null @@ -1,55 +0,0 @@ -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, - }) -} diff --git a/plugin-gosdk/host_test.go b/plugin-gosdk/host_test.go deleted file mode 100644 index 8927a698..00000000 --- a/plugin-gosdk/host_test.go +++ /dev/null @@ -1,42 +0,0 @@ -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() - } -} diff --git a/plugin-gosdk/main.go b/plugin-gosdk/main.go deleted file mode 100644 index 73b7f8d3..00000000 --- a/plugin-gosdk/main.go +++ /dev/null @@ -1,63 +0,0 @@ -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() -} diff --git a/plugin-gosdk/plugin/debugtalk.go b/plugin-gosdk/plugin/debugtalk.go deleted file mode 100644 index acf09ec2..00000000 --- a/plugin-gosdk/plugin/debugtalk.go +++ /dev/null @@ -1,38 +0,0 @@ -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() -} diff --git a/plugin.go b/plugin.go index 8de40078..389d58e3 100644 --- a/plugin.go +++ b/plugin.go @@ -12,15 +12,16 @@ import ( "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" - pluginSDK "github.com/httprunner/hrp/plugin-gosdk" + pluginHost "github.com/httprunner/hrp/plugin/host" + pluginShared "github.com/httprunner/hrp/plugin/shared" ) type pluginFile string const ( - goPluginFile pluginFile = pluginSDK.Name + ".so" // built from go plugin - hashicorpGoPluginFile pluginFile = pluginSDK.Name + ".bin" // built from hashicorp go plugin - hashicorpPyPluginFile pluginFile = pluginSDK.Name + ".py" + goPluginFile pluginFile = pluginShared.Name + ".so" // built from go plugin + hashicorpGoPluginFile pluginFile = pluginShared.Name + ".bin" // built from hashicorp go plugin + hashicorpPyPluginFile pluginFile = pluginShared.Name + ".py" ) type hrpPlugin interface { @@ -91,7 +92,7 @@ func (p *goPlugin) has(funcName string) bool { func (p *goPlugin) call(funcName string, args ...interface{}) (interface{}, error) { fn := p.cachedFunctions[funcName] - return callFunc(fn, args...) + return pluginShared.CallFunc(fn, args...) } func (p *goPlugin) quit() error { @@ -101,13 +102,13 @@ func (p *goPlugin) quit() error { // hashicorpPlugin implements hashicorp/go-plugin type hashicorpPlugin struct { - pluginSDK.FuncCaller + pluginShared.FuncCaller cachedFunctions map[string]bool // cache loaded functions to improve performance } func (p *hashicorpPlugin) init(path string) error { - f, err := pluginSDK.Init(path) + f, err := pluginHost.Init(path) if err != nil { log.Error().Err(err).Str("path", path).Msg("load go hashicorp plugin failed") return err @@ -147,7 +148,7 @@ func (p *hashicorpPlugin) call(funcName string, args ...interface{}) (interface{ func (p *hashicorpPlugin) quit() error { // kill hashicorp plugin process - pluginSDK.Quit() + pluginHost.Quit() return nil } @@ -230,49 +231,5 @@ func (p *parser) callFunc(funcName string, arguments ...interface{}) (interface{ fn := reflect.ValueOf(function) // call with builtin function - return callFunc(fn, arguments...) -} - -// callFunc calls function with arguments -// it is used when calling go plugin or builtin functions -func callFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { - fnArgsNum := fn.Type().NumIn() - if fnArgsNum > 0 && fn.Type().In(fnArgsNum-1).Kind() == reflect.Slice { - // last argument is slice, do not check arguments number - // e.g. ...interface{} - // e.g. a, b string, c ...interface{} - } else if fnArgsNum != len(args) { - // function arguments not match - return nil, fmt.Errorf("function arguments number not match") - } - // arguments do not have slice, and arguments number matched - - argumentsValue := make([]reflect.Value, len(args)) - for index, argument := range args { - if argument == nil { - argumentsValue[index] = reflect.Zero(fn.Type().In(index)) - } else { - argumentsValue[index] = reflect.ValueOf(args[index]) - } - } - - resultValues := fn.Call(argumentsValue) - if resultValues == nil { - // no returns - return nil, nil - } else if len(resultValues) == 2 { - // return two arguments: interface{}, error - if resultValues[1].Interface() != nil { - return resultValues[0].Interface(), resultValues[1].Interface().(error) - } else { - return resultValues[0].Interface(), nil - } - } else if len(resultValues) == 1 { - // return one arguments: interface{} - return resultValues[0].Interface(), nil - } else { - // return more than 2 arguments, unexpected - err := fmt.Errorf("function should return at most 2 arguments") - return nil, err - } + return pluginShared.CallFunc(fn, arguments...) } diff --git a/plugin/examples/debugtalk.go b/plugin/examples/debugtalk.go new file mode 100644 index 00000000..d9d52d6d --- /dev/null +++ b/plugin/examples/debugtalk.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + + plugin "github.com/httprunner/hrp/plugin" +) + +func SumTwoInt(a, b int) int { + return a + b +} + +func SumInts(args ...int) int { + var sum int + for _, arg := range args { + sum += arg + } + return sum +} + +func Sum(args ...interface{}) (interface{}, error) { + var sum float64 + for _, arg := range args { + switch v := arg.(type) { + case int: + sum += float64(v) + case float64: + sum += v + default: + return nil, fmt.Errorf("unexpected type: %T", arg) + } + } + return sum, nil +} + +func SumTwoString(a, b string) string { + return a + b +} + +func SumStrings(s ...string) string { + var sum string + for _, arg := range s { + sum += arg + } + return sum +} + +func Concatenate(args ...interface{}) (interface{}, error) { + var result string + for _, arg := range args { + result += fmt.Sprintf("%v", arg) + } + return result, nil +} + +// register functions and build to plugin binary +func main() { + plugin.Register("sum_ints", SumInts) + plugin.Register("sum_two_int", SumTwoInt) + plugin.Register("sum", Sum) + plugin.Register("sum_two_string", SumTwoString) + plugin.Register("sum_strings", SumStrings) + plugin.Register("concatenate", Concatenate) + plugin.Serve() +} diff --git a/plugin/examples/plugin_test.go b/plugin/examples/plugin_test.go new file mode 100644 index 00000000..2164ceee --- /dev/null +++ b/plugin/examples/plugin_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/httprunner/hrp/plugin/host" +) + +func TestMain(m *testing.M) { + fmt.Println("[TestMain] build go plugin") + cmd := exec.Command("go", "build", "-o=debugtalk.bin", "./debugtalk.go") + if err := cmd.Run(); err != nil { + panic(err) + } + os.Exit(m.Run()) +} + +func TestInitHashicorpPlugin(t *testing.T) { + f, err := host.Init("./debugtalk.bin") + if err != nil { + t.Fatal(err) + } + defer host.Quit() + + v1, err := f.GetNames() + if err != nil { + t.Fatal(err) + } + if !assert.Contains(t, v1, "sum_ints") { + t.Fatal(err) + } + if !assert.Contains(t, v1, "concatenate") { + t.Fatal(err) + } + + var v2 interface{} + v2, err = f.Call("sum_ints", 1, 2, 3, 4) + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, 10, v2) { + t.Fail() + } + v2, err = f.Call("sum_two_int", 1, 2) + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, 3, v2) { + t.Fail() + } + v2, err = f.Call("sum", 1, 2, 3.4, 5) + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, 11.4, v2) { + t.Fail() + } + + var v3 interface{} + v3, err = f.Call("sum_two_string", "a", "b") + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, "ab", v3) { + t.Fail() + } + v3, err = f.Call("sum_strings", "a", "b", "c") + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, "abc", v3) { + t.Fail() + } + + v3, err = f.Call("concatenate", "a", 2, "c", 3.4) + if err != nil { + t.Fatal(err) + } + if !assert.Equal(t, "a2c3.4", v3) { + t.Fail() + } +} diff --git a/plugin/host/host.go b/plugin/host/host.go new file mode 100644 index 00000000..e815c7e5 --- /dev/null +++ b/plugin/host/host.go @@ -0,0 +1,50 @@ +package host + +import ( + "os" + "os/exec" + + hclog "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + + "github.com/httprunner/hrp/plugin/shared" +) + +var client *plugin.Client + +func Init(path string) (shared.FuncCaller, error) { + // launch the plugin process + client = plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: shared.HandshakeConfig, + Plugins: map[string]plugin.Plugin{ + shared.Name: &shared.HashicorpPlugin{}, + }, + Cmd: exec.Command(path), + Logger: hclog.New(&hclog.LoggerOptions{ + Name: shared.Name, + Output: os.Stdout, + Level: hclog.Info, + }), + }) + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + // Request the plugin + raw, err := rpcClient.Dispense(shared.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.(shared.FuncCaller) + return function, nil +} + +func Quit() { + client.Kill() +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 00000000..c9301795 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,67 @@ +package plugin + +import ( + "fmt" + "os" + "reflect" + + hclog "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + + pluginShared "github.com/httprunner/hrp/plugin/shared" +) + +// functionsMap stores plugin functions +type functionsMap map[string]reflect.Value + +// functionPlugin implements the FuncCaller interface +type functionPlugin struct { + logger hclog.Logger + functions functionsMap +} + +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) + + fn, ok := p.functions[funcName] + if !ok { + return nil, fmt.Errorf("function %s not found", funcName) + } + + return pluginShared.CallFunc(fn, args...) +} + +var functions = make(functionsMap) + +func Register(funcName string, fn interface{}) { + if _, ok := functions[funcName]; ok { + return + } + functions[funcName] = reflect.ValueOf(fn) +} + +func Serve() { + funcPlugin := &functionPlugin{ + logger: hclog.New(&hclog.LoggerOptions{ + Name: pluginShared.Name, + Output: os.Stdout, + Level: hclog.Info, + }), + functions: functions, + } + var pluginMap = map[string]plugin.Plugin{ + pluginShared.Name: &pluginShared.HashicorpPlugin{Impl: funcPlugin}, + } + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: pluginShared.HandshakeConfig, + Plugins: pluginMap, + }) +} diff --git a/plugin/shared/config.go b/plugin/shared/config.go new file mode 100644 index 00000000..423688b1 --- /dev/null +++ b/plugin/shared/config.go @@ -0,0 +1,15 @@ +package shared + +import "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, +} diff --git a/plugin-gosdk/interface.go b/plugin/shared/interface.go similarity index 88% rename from plugin-gosdk/interface.go rename to plugin/shared/interface.go index 5853e397..9f334808 100644 --- a/plugin-gosdk/interface.go +++ b/plugin/shared/interface.go @@ -1,4 +1,4 @@ -package plugin +package shared import ( "encoding/gob" @@ -83,23 +83,21 @@ func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error { 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") + log.Error().Err(err).Interface("args", args).Msg("function execution failed") return err } return nil } -// hashicorpPlugin implements hashicorp's plugin.Plugin. -type hashicorpPlugin struct { +// HashicorpPlugin implements hashicorp's plugin.Plugin. +type HashicorpPlugin struct { Impl FuncCaller } -func (p *hashicorpPlugin) Server(*plugin.MuxBroker) (interface{}, error) { +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) { +func (HashicorpPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { return &functionRPC{client: c}, nil } diff --git a/plugin/shared/utils.go b/plugin/shared/utils.go new file mode 100644 index 00000000..a33a060b --- /dev/null +++ b/plugin/shared/utils.go @@ -0,0 +1,50 @@ +package shared + +import ( + "fmt" + "reflect" +) + +// CallFunc calls function with arguments +// it is used when calling go plugin or builtin functions +func CallFunc(fn reflect.Value, args ...interface{}) (interface{}, error) { + fnArgsNum := fn.Type().NumIn() + if fnArgsNum > 0 && fn.Type().In(fnArgsNum-1).Kind() == reflect.Slice { + // last argument is slice, do not check arguments number + // e.g. ...interface{} + // e.g. a, b string, c ...interface{} + } else if fnArgsNum != len(args) { + // function arguments not match + return nil, fmt.Errorf("function arguments number not match, expect %d, got %d", fnArgsNum, len(args)) + } + // arguments do not have slice, and arguments number matched + + argumentsValue := make([]reflect.Value, len(args)) + for index, argument := range args { + if argument == nil { + argumentsValue[index] = reflect.Zero(fn.Type().In(index)) + } else { + argumentsValue[index] = reflect.ValueOf(args[index]) + } + } + + resultValues := fn.Call(argumentsValue) + if resultValues == nil { + // no returns + return nil, nil + } else if len(resultValues) == 2 { + // return two arguments: interface{}, error + if resultValues[1].Interface() != nil { + return resultValues[0].Interface(), resultValues[1].Interface().(error) + } else { + return resultValues[0].Interface(), nil + } + } else if len(resultValues) == 1 { + // return one arguments: interface{} + return resultValues[0].Interface(), nil + } else { + // return more than 2 arguments, unexpected + err := fmt.Errorf("function should return at most 2 arguments") + return nil, err + } +}