From 4d64f2d7b64794c7a34c47fe711f6bc5d2550efb Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 3 Mar 2022 15:06:16 +0800 Subject: [PATCH 1/6] refactor: relocate files --- plugin/inner/interface.go | 14 ++++++++++++++ plugin/inner/{host.go => rpc.go} | 13 ------------- 2 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 plugin/inner/interface.go rename plugin/inner/{host.go => rpc.go} (78%) diff --git a/plugin/inner/interface.go b/plugin/inner/interface.go new file mode 100644 index 00000000..ddaafec4 --- /dev/null +++ b/plugin/inner/interface.go @@ -0,0 +1,14 @@ +package pluginInternal + +// 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 +} + +type IPlugin interface { + Init(path string) error // init plugin + Has(funcName string) bool // check if plugin has function + Call(funcName string, args ...interface{}) (interface{}, error) // call function + Quit() error // quit plugin +} diff --git a/plugin/inner/host.go b/plugin/inner/rpc.go similarity index 78% rename from plugin/inner/host.go rename to plugin/inner/rpc.go index a8422a38..804bc5a3 100644 --- a/plugin/inner/host.go +++ b/plugin/inner/rpc.go @@ -18,12 +18,6 @@ type funcData struct { 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 @@ -101,10 +95,3 @@ func (p *HRPPlugin) Server(*plugin.MuxBroker) (interface{}, error) { func (HRPPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { return &functionRPC{client: c}, nil } - -type IPlugin interface { - Init(path string) error // init plugin - Has(funcName string) bool // check if plugin has function - Call(funcName string, args ...interface{}) (interface{}, error) // call function - Quit() error // quit plugin -} From f312535908da73c65207cc1d05a3e76e4aacb712 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 16:13:38 +0800 Subject: [PATCH 2/6] feat: support hashicorp plugin in gPRC mode --- plugin/inner/config.go | 2 + plugin/inner/grpc.go | 108 ++++++++++ plugin/inner/hashicorp_plugin.go | 12 +- plugin/inner/rpc.go | 28 +-- plugin/plugin.go | 36 +++- plugin/proto/README.md | 10 + plugin/proto/debugtalk.pb.go | 341 ++++++++++++++++++++++++++++++ plugin/proto/debugtalk.proto | 24 +++ plugin/proto/debugtalk_grpc.pb.go | 137 ++++++++++++ 9 files changed, 677 insertions(+), 21 deletions(-) create mode 100644 plugin/inner/grpc.go create mode 100644 plugin/proto/README.md create mode 100644 plugin/proto/debugtalk.pb.go create mode 100644 plugin/proto/debugtalk.proto create mode 100644 plugin/proto/debugtalk_grpc.pb.go diff --git a/plugin/inner/config.go b/plugin/inner/config.go index b5597fde..df013eb2 100644 --- a/plugin/inner/config.go +++ b/plugin/inner/config.go @@ -3,6 +3,8 @@ package pluginInternal import "github.com/hashicorp/go-plugin" const PluginName = "debugtalk" +const RPCPluginName = PluginName + "_rpc" +const GRPCPluginName = PluginName + "_grpc" // handshakeConfigs are used to just do a basic handshake between // a plugin and host. If the handshake fails, a user friendly error is shown. diff --git a/plugin/inner/grpc.go b/plugin/inner/grpc.go new file mode 100644 index 00000000..5a1988ba --- /dev/null +++ b/plugin/inner/grpc.go @@ -0,0 +1,108 @@ +package pluginInternal + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/go-plugin" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "google.golang.org/grpc" + + "github.com/httprunner/hrp/plugin/proto" +) + +// functionGRPCClient runs on the host side, it implements FuncCaller interface +type functionGRPCClient struct { + client proto.DebugTalkClient +} + +func (m *functionGRPCClient) GetNames() ([]string, error) { + log.Info().Msg("function GetNames called on host side") + resp, err := m.client.GetNames(context.Background(), &proto.Empty{}) + if err != nil { + log.Error().Err(err).Msg("gRPC call GetNames() failed") + return nil, err + } + return resp.Names, err +} + +func (m *functionGRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { + log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via gRPC") + + req := &proto.CallRequest{ + Name: funcName, + } + funcArgBytes, err := json.Marshal(funcArgs) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal Call() funcArgs") + } + req.Args = funcArgBytes + + response, err := m.client.Call(context.Background(), req) + if err != nil { + log.Error().Err(err). + Str("funcName", funcName).Interface("funcArgs", funcArgs). + Msg("gRPC Call() failed") + return nil, err + } + + var resp interface{} + err = json.Unmarshal(response.Value, &resp) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal Call() response") + } + return resp, nil +} + +// Here is the gRPC server that functionGRPCClient talks to. +type functionGRPCServer struct { + proto.UnimplementedDebugTalkServer + Impl FuncCaller +} + +func (m *functionGRPCServer) GetNames(ctx context.Context, req *proto.Empty) (*proto.GetNamesResponse, error) { + log.Info().Interface("req", req).Msg("gRPC GetNames() called on plugin side") + v, err := m.Impl.GetNames() + if err != nil { + log.Error().Err(err).Msg("gRPC GetNames execution failed") + return nil, err + } + return &proto.GetNamesResponse{Names: v}, err +} + +func (m *functionGRPCServer) Call(ctx context.Context, req *proto.CallRequest) (*proto.CallResponse, error) { + var funcArgs []interface{} + if err := json.Unmarshal(req.Args, &funcArgs); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal Call() funcArgs") + } + + log.Info().Interface("req", req).Msg("gRPC Call() called on plugin side") + + v, err := m.Impl.Call(req.Name, funcArgs...) + if err != nil { + log.Error().Err(err).Interface("req", req).Msg("gRPC Call() execution failed") + return nil, err + } + + value, err := json.Marshal(v) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal Call() response") + } + return &proto.CallResponse{Value: value}, err +} + +// HRPPlugin implements hashicorp's plugin.GRPCPlugin. +type GRPCPlugin struct { + plugin.Plugin + Impl FuncCaller +} + +func (p *GRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterDebugTalkServer(s, &functionGRPCServer{Impl: p.Impl}) + return nil +} + +func (p *GRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &functionGRPCClient{client: proto.NewDebugTalkClient(c)}, nil +} diff --git a/plugin/inner/hashicorp_plugin.go b/plugin/inner/hashicorp_plugin.go index 246ba0ef..1e0996d9 100644 --- a/plugin/inner/hashicorp_plugin.go +++ b/plugin/inner/hashicorp_plugin.go @@ -19,8 +19,9 @@ type HashicorpPlugin struct { } func (p *HashicorpPlugin) Init(path string) error { + pluginName := GRPCPluginName loggerOptions := &hclog.LoggerOptions{ - Name: PluginName, + Name: pluginName, Output: os.Stdout, } if p.logOn { @@ -32,10 +33,15 @@ func (p *HashicorpPlugin) Init(path string) error { client = plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: HandshakeConfig, Plugins: map[string]plugin.Plugin{ - PluginName: &HRPPlugin{}, + RPCPluginName: &RPCPlugin{}, + GRPCPluginName: &GRPCPlugin{}, }, Cmd: exec.Command(path), Logger: hclog.New(loggerOptions), + AllowedProtocols: []plugin.Protocol{ + plugin.ProtocolNetRPC, + plugin.ProtocolGRPC, + }, }) // Connect via RPC @@ -46,7 +52,7 @@ func (p *HashicorpPlugin) Init(path string) error { } // Request the plugin - raw, err := rpcClient.Dispense(PluginName) + raw, err := rpcClient.Dispense(pluginName) if err != nil { log.Error().Err(err).Msg("request plugin failed") return err diff --git a/plugin/inner/rpc.go b/plugin/inner/rpc.go index 804bc5a3..190ac4e5 100644 --- a/plugin/inner/rpc.go +++ b/plugin/inner/rpc.go @@ -18,12 +18,12 @@ type funcData struct { Args []interface{} // function arguments } -// functionRPC runs on the host side. -type functionRPC struct { +// functionRPCClient runs on the host side, it implements FuncCaller interface +type functionRPCClient struct { client *rpc.Client } -func (g *functionRPC) GetNames() ([]string, error) { +func (g *functionRPCClient) GetNames() ([]string, error) { var resp []string err := g.client.Call("Plugin.GetNames", new(interface{}), &resp) if err != nil { @@ -34,7 +34,7 @@ func (g *functionRPC) GetNames() ([]string, error) { } // host -> plugin -func (g *functionRPC) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { +func (g *functionRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via RPC") f := funcData{ Name: funcName, @@ -47,7 +47,7 @@ func (g *functionRPC) Call(funcName string, funcArgs ...interface{}) (interface{ if err != nil { log.Error().Err(err). Str("funcName", funcName).Interface("funcArgs", funcArgs). - Msg("rpc call Call() failed") + Msg("rpc Call() failed") return nil, err } return resp, nil @@ -60,11 +60,11 @@ type functionRPCServer struct { // plugin execution func (s *functionRPCServer) GetNames(args interface{}, resp *[]string) error { - log.Info().Interface("args", args).Msg("GetNames called on plugin side") + log.Info().Interface("args", args).Msg("rpc GetNames() called on plugin side") var err error *resp, err = s.Impl.GetNames() if err != nil { - log.Error().Err(err).Msg("GetNames execution failed") + log.Error().Err(err).Msg("rpc GetNames() execution failed") return err } return nil @@ -72,26 +72,26 @@ func (s *functionRPCServer) GetNames(args interface{}, resp *[]string) error { // plugin execution func (s *functionRPCServer) Call(args interface{}, resp *interface{}) error { - log.Info().Interface("args", args).Msg("function called on plugin side") + log.Info().Interface("args", args).Msg("rpc Call() 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") + log.Error().Err(err).Interface("args", args).Msg("rpc Call() execution failed") return err } return nil } -// HRPPlugin implements hashicorp's plugin.Plugin. -type HRPPlugin struct { +// RPCPlugin implements hashicorp's plugin.Plugin. +type RPCPlugin struct { Impl FuncCaller } -func (p *HRPPlugin) Server(*plugin.MuxBroker) (interface{}, error) { +func (p *RPCPlugin) Server(*plugin.MuxBroker) (interface{}, error) { return &functionRPCServer{Impl: p.Impl}, nil } -func (HRPPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { - return &functionRPC{client: c}, nil +func (RPCPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &functionRPCClient{client: c}, nil } diff --git a/plugin/plugin.go b/plugin/plugin.go index 14435c3a..a2dc7c22 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -7,6 +7,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/rs/zerolog/log" pluginInternal "github.com/httprunner/hrp/plugin/inner" ) @@ -50,21 +51,48 @@ func Register(funcName string, fn interface{}) { functions[funcName] = reflect.ValueOf(fn) } -// Serve starts a plugin server process. -func Serve() { +// Serve starts a plugin server process in RPC mode. +func ServeRPC() { + log.Info().Msg("start plugin server in RPC mode") funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ - Name: pluginInternal.PluginName, + Name: pluginInternal.RPCPluginName, Output: os.Stdout, Level: hclog.Info, }), functions: functions, } var pluginMap = map[string]plugin.Plugin{ - pluginInternal.PluginName: &pluginInternal.HRPPlugin{Impl: funcPlugin}, + pluginInternal.RPCPluginName: &pluginInternal.RPCPlugin{Impl: funcPlugin}, } + // start RPC server plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: pluginInternal.HandshakeConfig, Plugins: pluginMap, }) } + +// Serve starts a plugin server process in gRPC mode. +func ServeGRPC() { + log.Info().Msg("start plugin server in gRPC mode") + funcPlugin := &functionPlugin{ + logger: hclog.New(&hclog.LoggerOptions{ + Name: pluginInternal.GRPCPluginName, + Output: os.Stdout, + Level: hclog.Info, + }), + functions: functions, + } + var pluginMap = map[string]plugin.Plugin{ + pluginInternal.GRPCPluginName: &pluginInternal.GRPCPlugin{Impl: funcPlugin}, + } + // start gRPC server + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: pluginInternal.HandshakeConfig, + Plugins: pluginMap, + GRPCServer: plugin.DefaultGRPCServer, + }) +} + +// default to run plugin in gRPC mode +var Serve = ServeGRPC diff --git a/plugin/proto/README.md b/plugin/proto/README.md new file mode 100644 index 00000000..acfb3918 --- /dev/null +++ b/plugin/proto/README.md @@ -0,0 +1,10 @@ + +## Updating the Protocol + +If you update the protocol buffers file, you can regenerate the file using the following command from the project root directory. You do not need to run this if you're just using the plugin. + +For Go: + +```bash +$ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative plugin/proto/debugtalk.proto +``` diff --git a/plugin/proto/debugtalk.pb.go b/plugin/proto/debugtalk.pb.go new file mode 100644 index 00000000..cb918ee4 --- /dev/null +++ b/plugin/proto/debugtalk.pb.go @@ -0,0 +1,341 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.19.4 +// source: plugin/proto/debugtalk.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Empty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Empty) Reset() { + *x = Empty{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_plugin_proto_debugtalk_proto_rawDescGZIP(), []int{0} +} + +type GetNamesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Names []string `protobuf:"bytes,1,rep,name=names,proto3" json:"names,omitempty"` +} + +func (x *GetNamesResponse) Reset() { + *x = GetNamesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetNamesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNamesResponse) ProtoMessage() {} + +func (x *GetNamesResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNamesResponse.ProtoReflect.Descriptor instead. +func (*GetNamesResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_debugtalk_proto_rawDescGZIP(), []int{1} +} + +func (x *GetNamesResponse) GetNames() []string { + if x != nil { + return x.Names + } + return nil +} + +type CallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Args []byte `protobuf:"bytes,2,opt,name=args,proto3" json:"args,omitempty"` // []interface{} +} + +func (x *CallRequest) Reset() { + *x = CallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallRequest) ProtoMessage() {} + +func (x *CallRequest) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallRequest.ProtoReflect.Descriptor instead. +func (*CallRequest) Descriptor() ([]byte, []int) { + return file_plugin_proto_debugtalk_proto_rawDescGZIP(), []int{2} +} + +func (x *CallRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CallRequest) GetArgs() []byte { + if x != nil { + return x.Args + } + return nil +} + +type CallResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value []byte `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` // interface{} +} + +func (x *CallResponse) Reset() { + *x = CallResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallResponse) ProtoMessage() {} + +func (x *CallResponse) ProtoReflect() protoreflect.Message { + mi := &file_plugin_proto_debugtalk_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallResponse.ProtoReflect.Descriptor instead. +func (*CallResponse) Descriptor() ([]byte, []int) { + return file_plugin_proto_debugtalk_proto_rawDescGZIP(), []int{3} +} + +func (x *CallResponse) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + +var File_plugin_proto_debugtalk_proto protoreflect.FileDescriptor + +var file_plugin_proto_debugtalk_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, + 0x65, 0x62, 0x75, 0x67, 0x74, 0x61, 0x6c, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x28, + 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x35, 0x0a, 0x0b, 0x43, 0x61, 0x6c, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, + 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x22, + 0x24, 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x6f, 0x0a, 0x09, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x61, + 0x6c, 0x6b, 0x12, 0x31, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x0c, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x12, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x2f, + 0x68, 0x72, 0x70, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_plugin_proto_debugtalk_proto_rawDescOnce sync.Once + file_plugin_proto_debugtalk_proto_rawDescData = file_plugin_proto_debugtalk_proto_rawDesc +) + +func file_plugin_proto_debugtalk_proto_rawDescGZIP() []byte { + file_plugin_proto_debugtalk_proto_rawDescOnce.Do(func() { + file_plugin_proto_debugtalk_proto_rawDescData = protoimpl.X.CompressGZIP(file_plugin_proto_debugtalk_proto_rawDescData) + }) + return file_plugin_proto_debugtalk_proto_rawDescData +} + +var file_plugin_proto_debugtalk_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_plugin_proto_debugtalk_proto_goTypes = []interface{}{ + (*Empty)(nil), // 0: proto.Empty + (*GetNamesResponse)(nil), // 1: proto.GetNamesResponse + (*CallRequest)(nil), // 2: proto.CallRequest + (*CallResponse)(nil), // 3: proto.CallResponse +} +var file_plugin_proto_debugtalk_proto_depIdxs = []int32{ + 0, // 0: proto.DebugTalk.GetNames:input_type -> proto.Empty + 2, // 1: proto.DebugTalk.Call:input_type -> proto.CallRequest + 1, // 2: proto.DebugTalk.GetNames:output_type -> proto.GetNamesResponse + 3, // 3: proto.DebugTalk.Call:output_type -> proto.CallResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_plugin_proto_debugtalk_proto_init() } +func file_plugin_proto_debugtalk_proto_init() { + if File_plugin_proto_debugtalk_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_plugin_proto_debugtalk_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Empty); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugin_proto_debugtalk_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetNamesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugin_proto_debugtalk_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_plugin_proto_debugtalk_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CallResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_plugin_proto_debugtalk_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_plugin_proto_debugtalk_proto_goTypes, + DependencyIndexes: file_plugin_proto_debugtalk_proto_depIdxs, + MessageInfos: file_plugin_proto_debugtalk_proto_msgTypes, + }.Build() + File_plugin_proto_debugtalk_proto = out.File + file_plugin_proto_debugtalk_proto_rawDesc = nil + file_plugin_proto_debugtalk_proto_goTypes = nil + file_plugin_proto_debugtalk_proto_depIdxs = nil +} diff --git a/plugin/proto/debugtalk.proto b/plugin/proto/debugtalk.proto new file mode 100644 index 00000000..10ec81c9 --- /dev/null +++ b/plugin/proto/debugtalk.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +package proto; + +option go_package = "github.com/httprunner/hrp/plugin/proto"; + +message Empty {} + +message GetNamesResponse { + repeated string names = 1; +} + +message CallRequest { + string name = 1; + bytes args = 2; // []interface{} +} + +message CallResponse { + bytes value = 1; // interface{} +} + +service DebugTalk { + rpc GetNames(Empty) returns (GetNamesResponse); + rpc Call(CallRequest) returns (CallResponse); +} diff --git a/plugin/proto/debugtalk_grpc.pb.go b/plugin/proto/debugtalk_grpc.pb.go new file mode 100644 index 00000000..166fe536 --- /dev/null +++ b/plugin/proto/debugtalk_grpc.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// DebugTalkClient is the client API for DebugTalk service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DebugTalkClient interface { + GetNames(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetNamesResponse, error) + Call(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallResponse, error) +} + +type debugTalkClient struct { + cc grpc.ClientConnInterface +} + +func NewDebugTalkClient(cc grpc.ClientConnInterface) DebugTalkClient { + return &debugTalkClient{cc} +} + +func (c *debugTalkClient) GetNames(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GetNamesResponse, error) { + out := new(GetNamesResponse) + err := c.cc.Invoke(ctx, "/proto.DebugTalk/GetNames", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *debugTalkClient) Call(ctx context.Context, in *CallRequest, opts ...grpc.CallOption) (*CallResponse, error) { + out := new(CallResponse) + err := c.cc.Invoke(ctx, "/proto.DebugTalk/Call", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DebugTalkServer is the server API for DebugTalk service. +// All implementations must embed UnimplementedDebugTalkServer +// for forward compatibility +type DebugTalkServer interface { + GetNames(context.Context, *Empty) (*GetNamesResponse, error) + Call(context.Context, *CallRequest) (*CallResponse, error) + mustEmbedUnimplementedDebugTalkServer() +} + +// UnimplementedDebugTalkServer must be embedded to have forward compatible implementations. +type UnimplementedDebugTalkServer struct { +} + +func (UnimplementedDebugTalkServer) GetNames(context.Context, *Empty) (*GetNamesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetNames not implemented") +} +func (UnimplementedDebugTalkServer) Call(context.Context, *CallRequest) (*CallResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Call not implemented") +} +func (UnimplementedDebugTalkServer) mustEmbedUnimplementedDebugTalkServer() {} + +// UnsafeDebugTalkServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DebugTalkServer will +// result in compilation errors. +type UnsafeDebugTalkServer interface { + mustEmbedUnimplementedDebugTalkServer() +} + +func RegisterDebugTalkServer(s grpc.ServiceRegistrar, srv DebugTalkServer) { + s.RegisterService(&DebugTalk_ServiceDesc, srv) +} + +func _DebugTalk_GetNames_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DebugTalkServer).GetNames(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.DebugTalk/GetNames", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DebugTalkServer).GetNames(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DebugTalk_Call_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CallRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DebugTalkServer).Call(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.DebugTalk/Call", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DebugTalkServer).Call(ctx, req.(*CallRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// DebugTalk_ServiceDesc is the grpc.ServiceDesc for DebugTalk service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DebugTalk_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.DebugTalk", + HandlerType: (*DebugTalkServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetNames", + Handler: _DebugTalk_GetNames_Handler, + }, + { + MethodName: "Call", + Handler: _DebugTalk_Call_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "plugin/proto/debugtalk.proto", +} From b729b66be3c894a5cb5c56fb6fce749d173dc08d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 16:56:15 +0800 Subject: [PATCH 3/6] update docs --- docs/CHANGELOG.md | 7 ++++++- docs/cmd/hrp.md | 2 +- docs/cmd/hrp_boom.md | 2 +- docs/cmd/hrp_har2case.md | 2 +- docs/cmd/hrp_run.md | 2 +- docs/cmd/hrp_startproject.md | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cc54b050..10989d60 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,13 @@ # Release History -## v0.6.3 (2022-02-22) +## v0.6.3 (2022-03-04) +- refactor: replace hashicorp plugin from net/rpc to gPRC mode - feat: support customized setup/teardown hooks (variable assignment not supported) +- feat: add flag `--log-plugin` to turn on plugin logging +- change: add short flag `-c` for `--continue-on-failure` +- change: use `--log-requests-off` flag to turn off request & response details logging +- fix: support posting body in json array format ## v0.6.2 (2022-02-22) diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index e0e66c1d..b87593e9 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -33,4 +33,4 @@ Copyright 2021 debugtalk * [hrp run](hrp_run.md) - run API test * [hrp startproject](hrp_startproject.md) - create a scaffold project -###### Auto generated by spf13/cobra on 25-Feb-2022 +###### Auto generated by spf13/cobra on 4-Mar-2022 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index bc44844e..70f135a2 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -39,4 +39,4 @@ hrp boom [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 25-Feb-2022 +###### Auto generated by spf13/cobra on 4-Mar-2022 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index f9423ae4..492f5f61 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -23,4 +23,4 @@ hrp har2case $har_path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 25-Feb-2022 +###### Auto generated by spf13/cobra on 4-Mar-2022 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index f898999f..8ddd948c 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -34,4 +34,4 @@ hrp run $path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 25-Feb-2022 +###### Auto generated by spf13/cobra on 4-Mar-2022 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index 3f9654cb..ef87ae83 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -16,4 +16,4 @@ hrp startproject $project_name [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 25-Feb-2022 +###### Auto generated by spf13/cobra on 4-Mar-2022 From 7dd2ac9831eab1f9de5acc27e5bc0b2aabdc3527 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 17:39:15 +0800 Subject: [PATCH 4/6] refactor: replace json with json-iterator/go to improve performance --- convert.go | 3 ++- go.mod | 1 + go.sum | 5 +++++ internal/boomer/output.go | 3 ++- internal/boomer/stats.go | 3 ++- internal/builtin/function.go | 4 ++-- internal/har2case/core.go | 2 +- internal/json/json.go | 14 ++++++++++++++ parser.go | 6 +++--- plugin/inner/grpc.go | 2 +- response.go | 5 +++-- runner.go | 2 +- 12 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 internal/json/json.go diff --git a/convert.go b/convert.go index 396416f8..d0cdcfee 100644 --- a/convert.go +++ b/convert.go @@ -2,13 +2,14 @@ package hrp import ( "bytes" - "encoding/json" "fmt" "os" "path/filepath" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" + + "github.com/httprunner/hrp/internal/json" ) func loadFromJSON(path string) (*TCase, error) { diff --git a/go.mod b/go.mod index 3bbe6445..855fec71 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/httprunner/hrp/plugin v0.0.0 github.com/jinzhu/copier v0.3.2 github.com/jmespath/go-jmespath v0.4.0 + github.com/json-iterator/go v1.1.12 github.com/maja42/goval v1.2.1 github.com/mattn/go-runewidth v0.0.13 // indirect github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 7332da7e..806d0647 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,8 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -252,9 +254,12 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= diff --git a/internal/boomer/output.go b/internal/boomer/output.go index 2f05149d..a2aa6422 100644 --- a/internal/boomer/output.go +++ b/internal/boomer/output.go @@ -1,7 +1,6 @@ package boomer import ( - "encoding/json" "fmt" "os" "sort" @@ -13,6 +12,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/push" "github.com/rs/zerolog/log" + + "github.com/httprunner/hrp/internal/json" ) // Output is primarily responsible for printing test results to different destinations diff --git a/internal/boomer/stats.go b/internal/boomer/stats.go index 0f19f595..2d501781 100644 --- a/internal/boomer/stats.go +++ b/internal/boomer/stats.go @@ -1,8 +1,9 @@ package boomer import ( - "encoding/json" "time" + + "github.com/httprunner/hrp/internal/json" ) type transaction struct { diff --git a/internal/builtin/function.go b/internal/builtin/function.go index 78c2f9fe..70cff18e 100644 --- a/internal/builtin/function.go +++ b/internal/builtin/function.go @@ -5,7 +5,6 @@ import ( "crypto/md5" "encoding/csv" "encoding/hex" - "encoding/json" "fmt" "math" "math/rand" @@ -15,9 +14,10 @@ import ( "strings" "time" + "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" - "github.com/rs/zerolog/log" + "github.com/httprunner/hrp/internal/json" ) var Functions = map[string]interface{}{ diff --git a/internal/har2case/core.go b/internal/har2case/core.go index ed2ce221..4d2b93cc 100644 --- a/internal/har2case/core.go +++ b/internal/har2case/core.go @@ -2,7 +2,6 @@ package har2case import ( "encoding/base64" - "encoding/json" "fmt" "io" "net/url" @@ -17,6 +16,7 @@ import ( "github.com/httprunner/hrp" "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" + "github.com/httprunner/hrp/internal/json" ) const ( diff --git a/internal/json/json.go b/internal/json/json.go new file mode 100644 index 00000000..9a5649ae --- /dev/null +++ b/internal/json/json.go @@ -0,0 +1,14 @@ +package json + +import ( + jsoniter "github.com/json-iterator/go" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +var ( + Marshal = json.Marshal + MarshalIndent = json.MarshalIndent + Unmarshal = json.Unmarshal + NewDecoder = json.NewDecoder +) diff --git a/parser.go b/parser.go index 7cfccf55..8e1e8602 100644 --- a/parser.go +++ b/parser.go @@ -1,7 +1,7 @@ package hrp import ( - "encoding/json" + builtinJSON "encoding/json" "fmt" "net/url" "reflect" @@ -68,7 +68,7 @@ func (p *parser) parseData(raw interface{}, variablesMapping map[string]interfac switch rawValue.Kind() { case reflect.String: // json.Number - if rawValue, ok := raw.(json.Number); ok { + if rawValue, ok := raw.(builtinJSON.Number); ok { return parseJSONNumber(rawValue) } // other string @@ -108,7 +108,7 @@ func (p *parser) parseData(raw interface{}, variablesMapping map[string]interfac } } -func parseJSONNumber(raw json.Number) (interface{}, error) { +func parseJSONNumber(raw builtinJSON.Number) (interface{}, error) { if strings.Contains(raw.String(), ".") { // float64 return raw.Float64() diff --git a/plugin/inner/grpc.go b/plugin/inner/grpc.go index 5a1988ba..1c38a602 100644 --- a/plugin/inner/grpc.go +++ b/plugin/inner/grpc.go @@ -2,13 +2,13 @@ package pluginInternal import ( "context" - "encoding/json" "github.com/hashicorp/go-plugin" "github.com/pkg/errors" "github.com/rs/zerolog/log" "google.golang.org/grpc" + "github.com/httprunner/hrp/internal/json" "github.com/httprunner/hrp/plugin/proto" ) diff --git a/response.go b/response.go index 2861b119..895390f8 100644 --- a/response.go +++ b/response.go @@ -2,7 +2,7 @@ package hrp import ( "bytes" - "encoding/json" + builtinJSON "encoding/json" "fmt" "io" "net/http" @@ -15,6 +15,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/hrp/internal/builtin" + "github.com/httprunner/hrp/internal/json" ) func newResponseObject(t *testing.T, parser *parser, resp *http.Response) (*responseObject, error) { @@ -184,7 +185,7 @@ func (v *responseObject) searchJmespath(expr string) interface{} { log.Error().Str("expr", expr).Err(err).Msg("search jmespath failed") return expr // jmespath not found, return the expression } - if number, ok := checkValue.(json.Number); ok { + if number, ok := checkValue.(builtinJSON.Number); ok { checkNumber, err := parseJSONNumber(number) if err != nil { log.Error().Interface("json number", number).Err(err).Msg("convert json number failed") diff --git a/runner.go b/runner.go index 7dd0178b..c986e6b5 100644 --- a/runner.go +++ b/runner.go @@ -7,7 +7,6 @@ import ( "compress/gzip" "crypto/tls" _ "embed" - "encoding/json" "fmt" "html/template" "io" @@ -32,6 +31,7 @@ import ( "github.com/httprunner/hrp/internal/builtin" "github.com/httprunner/hrp/internal/ga" + "github.com/httprunner/hrp/internal/json" pluginInternal "github.com/httprunner/hrp/plugin/inner" ) From c27224a452b21d7ba29728fc84c8fdf1708abff6 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 17:54:46 +0800 Subject: [PATCH 5/6] update version --- cli/scripts/install.sh | 2 +- internal/version/init.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/scripts/install.sh b/cli/scripts/install.sh index 9a83846a..fe5efc8e 100644 --- a/cli/scripts/install.sh +++ b/cli/scripts/install.sh @@ -2,7 +2,7 @@ # install hrp with one shell command # bash -c "$(curl -ksSL https://httprunner.oss-cn-beijing.aliyuncs.com/install.sh)" -LATEST_VERSION="v0.6.3" +LATEST_VERSION="v0.7.0-beta" set -e diff --git a/internal/version/init.go b/internal/version/init.go index 3b9583b8..58e201aa 100644 --- a/internal/version/init.go +++ b/internal/version/init.go @@ -1,3 +1,3 @@ package version -const VERSION = "v0.6.3" +const VERSION = "v0.7.0-beta" From ea89a0b6a98b02c4596c9856c17f9547a9008e2c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 4 Mar 2022 20:34:24 +0800 Subject: [PATCH 6/6] feat: support switch hashicorp plugin with environment HRP_PLUGIN_TYPE --- docs/CHANGELOG.md | 2 +- internal/builtin/function.go | 4 ++-- internal/json/json.go | 1 + plugin/README.md | 12 ++++++++++++ plugin/inner/config.go | 22 +++++++++++++++++++++- plugin/inner/grpc.go | 10 +++++----- plugin/inner/hashicorp_plugin.go | 26 +++++++++++++++++++------- plugin/plugin.go | 17 ++++++++++++----- 8 files changed, 73 insertions(+), 21 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 25fdd559..8d10d6c3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,7 +2,7 @@ ## v0.7.0 (2022-03-04) -- refactor: replace hashicorp plugin from net/rpc to gPRC mode +- feat: both support gPRC(default) and net/rpc mode in hashicorp plugin, switch with environment `HRP_PLUGIN_TYPE` - refactor: replace builtin json library with json-iterator/go to improve performance ## v0.6.3 (2022-03-04) diff --git a/internal/builtin/function.go b/internal/builtin/function.go index 70cff18e..ea7a6720 100644 --- a/internal/builtin/function.go +++ b/internal/builtin/function.go @@ -96,7 +96,7 @@ func Dump2JSON(data interface{}, path string) error { return err } log.Info().Str("path", path).Msg("dump data to json") - file, _ := json.MarshalIndent(data, "", "\t") + file, _ := json.MarshalIndent(data, "", " ") err = os.WriteFile(path, file, 0644) if err != nil { log.Error().Err(err).Msg("dump json path failed") @@ -137,7 +137,7 @@ func FormatResponse(raw interface{}) interface{} { for key, value := range raw.(map[string]interface{}) { // convert value to json if key == "body" { - b, _ := json.MarshalIndent(&value, "", "\t") + b, _ := json.MarshalIndent(&value, "", " ") value = string(b) } formattedResponse[key] = value diff --git a/internal/json/json.go b/internal/json/json.go index 9a5649ae..ca3efbf5 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -4,6 +4,7 @@ import ( jsoniter "github.com/json-iterator/go" ) +// replace with third-party json library to improve performance var json = jsoniter.ConfigCompatibleWithStandardLibrary var ( diff --git a/plugin/README.md b/plugin/README.md index b83e2c10..91a3a601 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -99,6 +99,18 @@ Then, you can call your defined plugin function in your `YAML/JSON` testcase at } ``` +### rpc vs. gRPC + +HttpRunner+ has both supported `net/rpc` and `gRPC` in [hashicorp/plugin]. It is recommended to use `gRPC` and this is the default choice. + +If you want to run plugin in `net/rpc` mode, you can set an environment variable `HRP_PLUGIN_TYPE=rpc`. + +```bash +$ export HRP_PLUGIN_TYPE=rpc +$ hrp run examples/demo.json +$ hrp boom examples/demo.json +``` + ## go plugin The golang official plugin is only supported on Linux, FreeBSD, and macOS. And this solution also has many drawbacks. diff --git a/plugin/inner/config.go b/plugin/inner/config.go index df013eb2..791fee9a 100644 --- a/plugin/inner/config.go +++ b/plugin/inner/config.go @@ -1,6 +1,11 @@ package pluginInternal -import "github.com/hashicorp/go-plugin" +import ( + "os" + "strings" + + "github.com/hashicorp/go-plugin" +) const PluginName = "debugtalk" const RPCPluginName = PluginName + "_rpc" @@ -15,3 +20,18 @@ var HandshakeConfig = plugin.HandshakeConfig{ MagicCookieKey: "HttpRunnerPlus", MagicCookieValue: PluginName, } + +const hrpPluginTypeEnvName = "HRP_PLUGIN_TYPE" + +var hrpPluginType string + +func init() { + hrpPluginType = strings.ToLower(os.Getenv(hrpPluginTypeEnvName)) + if hrpPluginType == "" { + hrpPluginType = "grpc" // default + } +} + +func IsRPCPluginType() bool { + return hrpPluginType == "rpc" +} diff --git a/plugin/inner/grpc.go b/plugin/inner/grpc.go index 1c38a602..e2761b17 100644 --- a/plugin/inner/grpc.go +++ b/plugin/inner/grpc.go @@ -30,14 +30,14 @@ func (m *functionGRPCClient) GetNames() ([]string, error) { func (m *functionGRPCClient) Call(funcName string, funcArgs ...interface{}) (interface{}, error) { log.Info().Str("funcName", funcName).Interface("funcArgs", funcArgs).Msg("call function via gRPC") - req := &proto.CallRequest{ - Name: funcName, - } funcArgBytes, err := json.Marshal(funcArgs) if err != nil { return nil, errors.Wrap(err, "failed to marshal Call() funcArgs") } - req.Args = funcArgBytes + req := &proto.CallRequest{ + Name: funcName, + Args: funcArgBytes, + } response, err := m.client.Call(context.Background(), req) if err != nil { @@ -65,7 +65,7 @@ func (m *functionGRPCServer) GetNames(ctx context.Context, req *proto.Empty) (*p log.Info().Interface("req", req).Msg("gRPC GetNames() called on plugin side") v, err := m.Impl.GetNames() if err != nil { - log.Error().Err(err).Msg("gRPC GetNames execution failed") + log.Error().Err(err).Msg("gRPC GetNames() execution failed") return nil, err } return &proto.GetNamesResponse{Names: v}, err diff --git a/plugin/inner/hashicorp_plugin.go b/plugin/inner/hashicorp_plugin.go index 1e0996d9..7b4cbd39 100644 --- a/plugin/inner/hashicorp_plugin.go +++ b/plugin/inner/hashicorp_plugin.go @@ -1,11 +1,13 @@ package pluginInternal import ( + "fmt" "os" "os/exec" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" + "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -19,7 +21,14 @@ type HashicorpPlugin struct { } func (p *HashicorpPlugin) Init(path string) error { - pluginName := GRPCPluginName + var pluginName string + if IsRPCPluginType() { + pluginName = RPCPluginName + } else { + pluginName = GRPCPluginName + } + + // logger loggerOptions := &hclog.LoggerOptions{ Name: pluginName, Output: os.Stdout, @@ -29,6 +38,11 @@ func (p *HashicorpPlugin) Init(path string) error { } else { loggerOptions.Level = hclog.Info } + + // cmd + cmd := exec.Command(path) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", hrpPluginTypeEnvName, hrpPluginType)) + // launch the plugin process client = plugin.NewClient(&plugin.ClientConfig{ HandshakeConfig: HandshakeConfig, @@ -36,7 +50,7 @@ func (p *HashicorpPlugin) Init(path string) error { RPCPluginName: &RPCPlugin{}, GRPCPluginName: &GRPCPlugin{}, }, - Cmd: exec.Command(path), + Cmd: cmd, Logger: hclog.New(loggerOptions), AllowedProtocols: []plugin.Protocol{ plugin.ProtocolNetRPC, @@ -44,18 +58,16 @@ func (p *HashicorpPlugin) Init(path string) error { }, }) - // Connect via RPC + // Connect via RPC/gRPC rpcClient, err := client.Client() if err != nil { - log.Error().Err(err).Msg("connect plugin via RPC failed") - return err + return errors.Wrap(err, fmt.Sprintf("connect %s plugin failed", hrpPluginType)) } // Request the plugin raw, err := rpcClient.Dispense(pluginName) if err != nil { - log.Error().Err(err).Msg("request plugin failed") - return err + return errors.Wrap(err, fmt.Sprintf("request %s plugin failed", hrpPluginType)) } // We should have a Function now! This feels like a normal interface diff --git a/plugin/plugin.go b/plugin/plugin.go index a2dc7c22..fc32774b 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -51,8 +51,8 @@ func Register(funcName string, fn interface{}) { functions[funcName] = reflect.ValueOf(fn) } -// Serve starts a plugin server process in RPC mode. -func ServeRPC() { +// serveRPC starts a plugin server process in RPC mode. +func serveRPC() { log.Info().Msg("start plugin server in RPC mode") funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ @@ -72,8 +72,8 @@ func ServeRPC() { }) } -// Serve starts a plugin server process in gRPC mode. -func ServeGRPC() { +// serveGRPC starts a plugin server process in gRPC mode. +func serveGRPC() { log.Info().Msg("start plugin server in gRPC mode") funcPlugin := &functionPlugin{ logger: hclog.New(&hclog.LoggerOptions{ @@ -95,4 +95,11 @@ func ServeGRPC() { } // default to run plugin in gRPC mode -var Serve = ServeGRPC +func Serve() { + if pluginInternal.IsRPCPluginType() { + serveRPC() + } else { + // default + serveGRPC() + } +}