diff --git a/README.md b/README.md index ecc138649..41861a743 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## seer-project 项目结构: - +go tool pprof -http :8081 "http://127.0.0.1:8080/debug/pprof/profile" 目前仅有Logic服 主类位于 **server -> logic -> go -> LogicServer** diff --git a/blazing/common/api/Kick.pb.go b/blazing/common/api/Kick.pb.go new file mode 100644 index 000000000..1617ee6b4 --- /dev/null +++ b/blazing/common/api/Kick.pb.go @@ -0,0 +1,154 @@ +// 1.proto + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v4.25.3 +// source: manifest/proto/Kick.proto + +package api + +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 Kick struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name int32 `protobuf:"varint,1,opt,name=name,proto3" json:"name,omitempty"` //服务器端口 + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` // +} + +func (x *Kick) Reset() { + *x = Kick{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_Kick_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Kick) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Kick) ProtoMessage() {} + +func (x *Kick) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_Kick_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 Kick.ProtoReflect.Descriptor instead. +func (*Kick) Descriptor() ([]byte, []int) { + return file_manifest_proto_Kick_proto_rawDescGZIP(), []int{0} +} + +func (x *Kick) GetName() int32 { + if x != nil { + return x.Name + } + return 0 +} + +func (x *Kick) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +var File_manifest_proto_Kick_proto protoreflect.FileDescriptor + +var file_manifest_proto_Kick_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x4b, 0x69, 0x63, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, + 0x22, 0x2c, 0x0a, 0x04, 0x4b, 0x69, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x67, 0x65, 0x42, 0x14, + 0x5a, 0x12, 0x62, 0x6c, 0x61, 0x7a, 0x69, 0x6e, 0x67, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_manifest_proto_Kick_proto_rawDescOnce sync.Once + file_manifest_proto_Kick_proto_rawDescData = file_manifest_proto_Kick_proto_rawDesc +) + +func file_manifest_proto_Kick_proto_rawDescGZIP() []byte { + file_manifest_proto_Kick_proto_rawDescOnce.Do(func() { + file_manifest_proto_Kick_proto_rawDescData = protoimpl.X.CompressGZIP(file_manifest_proto_Kick_proto_rawDescData) + }) + return file_manifest_proto_Kick_proto_rawDescData +} + +var file_manifest_proto_Kick_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_manifest_proto_Kick_proto_goTypes = []any{ + (*Kick)(nil), // 0: api.Kick +} +var file_manifest_proto_Kick_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_manifest_proto_Kick_proto_init() } +func file_manifest_proto_Kick_proto_init() { + if File_manifest_proto_Kick_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_manifest_proto_Kick_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Kick); 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_manifest_proto_Kick_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_manifest_proto_Kick_proto_goTypes, + DependencyIndexes: file_manifest_proto_Kick_proto_depIdxs, + MessageInfos: file_manifest_proto_Kick_proto_msgTypes, + }.Build() + File_manifest_proto_Kick_proto = out.File + file_manifest_proto_Kick_proto_rawDesc = nil + file_manifest_proto_Kick_proto_goTypes = nil + file_manifest_proto_Kick_proto_depIdxs = nil +} diff --git a/blazing/common/api/Quit.pb.go b/blazing/common/api/Quit.pb.go new file mode 100644 index 000000000..e37c5ebf4 --- /dev/null +++ b/blazing/common/api/Quit.pb.go @@ -0,0 +1,153 @@ +// 1.proto + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v4.25.3 +// source: manifest/proto/Quit.proto + +package api + +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 Person struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` +} + +func (x *Person) Reset() { + *x = Person{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_Quit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Person) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Person) ProtoMessage() {} + +func (x *Person) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_Quit_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 Person.ProtoReflect.Descriptor instead. +func (*Person) Descriptor() ([]byte, []int) { + return file_manifest_proto_Quit_proto_rawDescGZIP(), []int{0} +} + +func (x *Person) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Person) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +var File_manifest_proto_Quit_proto protoreflect.FileDescriptor + +var file_manifest_proto_Quit_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x51, 0x75, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, + 0x22, 0x2e, 0x0a, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x67, 0x65, + 0x42, 0x14, 0x5a, 0x12, 0x62, 0x6c, 0x61, 0x7a, 0x69, 0x6e, 0x67, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_manifest_proto_Quit_proto_rawDescOnce sync.Once + file_manifest_proto_Quit_proto_rawDescData = file_manifest_proto_Quit_proto_rawDesc +) + +func file_manifest_proto_Quit_proto_rawDescGZIP() []byte { + file_manifest_proto_Quit_proto_rawDescOnce.Do(func() { + file_manifest_proto_Quit_proto_rawDescData = protoimpl.X.CompressGZIP(file_manifest_proto_Quit_proto_rawDescData) + }) + return file_manifest_proto_Quit_proto_rawDescData +} + +var file_manifest_proto_Quit_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_manifest_proto_Quit_proto_goTypes = []any{ + (*Person)(nil), // 0: api.Person +} +var file_manifest_proto_Quit_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] 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_manifest_proto_Quit_proto_init() } +func file_manifest_proto_Quit_proto_init() { + if File_manifest_proto_Quit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_manifest_proto_Quit_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Person); 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_manifest_proto_Quit_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_manifest_proto_Quit_proto_goTypes, + DependencyIndexes: file_manifest_proto_Quit_proto_depIdxs, + MessageInfos: file_manifest_proto_Quit_proto_msgTypes, + }.Build() + File_manifest_proto_Quit_proto = out.File + file_manifest_proto_Quit_proto_rawDesc = nil + file_manifest_proto_Quit_proto_goTypes = nil + file_manifest_proto_Quit_proto_depIdxs = nil +} diff --git a/build.bat b/build.bat index 67c7dc232..416572af3 100644 --- a/build.bat +++ b/build.bat @@ -1,2 +1,4 @@ gf pack modules/base/resource modules/base/packed/packed.go -p modules/base/resource +protoc --go_out=./ --go-grpc_out=./ manifest\proto\rpc.proto + diff --git a/common/api/rpc.pb.go b/common/api/rpc.pb.go new file mode 100644 index 000000000..cc1a5beef --- /dev/null +++ b/common/api/rpc.pb.go @@ -0,0 +1,798 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.20.0--rc1 +// source: manifest/proto/rpc.proto + +package api + +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) +) + +// 注册请求 - logic 用户登录后注册 +type RegisterUser struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Identity int32 `protobuf:"varint,1,opt,name=identity,proto3" json:"identity,omitempty"` // 客户端身份,进入后保存id->端口 实现通知踢人以及进程退出 + UserId int32 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 执行踢人操作的用户id +} + +func (x *RegisterUser) Reset() { + *x = RegisterUser{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterUser) ProtoMessage() {} + +func (x *RegisterUser) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_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 RegisterUser.ProtoReflect.Descriptor instead. +func (*RegisterUser) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterUser) GetIdentity() int32 { + if x != nil { + return x.Identity + } + return 0 +} + +func (x *RegisterUser) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +// 注册请求 - B客户端使用此消息向服务器注册 +type KickRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId int32 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // 执行踢人操作的用户id +} + +func (x *KickRequest) Reset() { + *x = KickRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *KickRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*KickRequest) ProtoMessage() {} + +func (x *KickRequest) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_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 KickRequest.ProtoReflect.Descriptor instead. +func (*KickRequest) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{1} +} + +func (x *KickRequest) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +// 函数描述符 +type FunctionDescriptor struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FunctionName string `protobuf:"bytes,1,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` // 函数名称 + InputType string `protobuf:"bytes,2,opt,name=input_type,json=inputType,proto3" json:"input_type,omitempty"` // 输入参数类型 + OutputType string `protobuf:"bytes,3,opt,name=output_type,json=outputType,proto3" json:"output_type,omitempty"` // 输出参数类型 + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` // 函数描述 +} + +func (x *FunctionDescriptor) Reset() { + *x = FunctionDescriptor{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FunctionDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionDescriptor) ProtoMessage() {} + +func (x *FunctionDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_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 FunctionDescriptor.ProtoReflect.Descriptor instead. +func (*FunctionDescriptor) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{2} +} + +func (x *FunctionDescriptor) GetFunctionName() string { + if x != nil { + return x.FunctionName + } + return "" +} + +func (x *FunctionDescriptor) GetInputType() string { + if x != nil { + return x.InputType + } + return "" +} + +func (x *FunctionDescriptor) GetOutputType() string { + if x != nil { + return x.OutputType + } + return "" +} + +func (x *FunctionDescriptor) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// 注册响应 +type RegisterResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // 注册是否成功 + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 消息描述 + RegistrationId string `protobuf:"bytes,3,opt,name=registration_id,json=registrationId,proto3" json:"registration_id,omitempty"` // 注册ID +} + +func (x *RegisterResponse) Reset() { + *x = RegisterResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterResponse) ProtoMessage() {} + +func (x *RegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_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 RegisterResponse.ProtoReflect.Descriptor instead. +func (*RegisterResponse) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{3} +} + +func (x *RegisterResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *RegisterResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *RegisterResponse) GetRegistrationId() string { + if x != nil { + return x.RegistrationId + } + return "" +} + +// 函数调用请求 - A客户端使用此消息请求调用B的函数 +type FunctionCallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TargetClientId string `protobuf:"bytes,1,opt,name=target_client_id,json=targetClientId,proto3" json:"target_client_id,omitempty"` // 目标客户端ID(B) + FunctionName string `protobuf:"bytes,2,opt,name=function_name,json=functionName,proto3" json:"function_name,omitempty"` // 要调用的函数名 + Parameters []byte `protobuf:"bytes,3,opt,name=parameters,proto3" json:"parameters,omitempty"` // 序列化后的函数参数 + CallId string `protobuf:"bytes,4,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // 调用ID,用于关联响应 +} + +func (x *FunctionCallRequest) Reset() { + *x = FunctionCallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FunctionCallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionCallRequest) ProtoMessage() {} + +func (x *FunctionCallRequest) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_proto_msgTypes[4] + 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 FunctionCallRequest.ProtoReflect.Descriptor instead. +func (*FunctionCallRequest) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{4} +} + +func (x *FunctionCallRequest) GetTargetClientId() string { + if x != nil { + return x.TargetClientId + } + return "" +} + +func (x *FunctionCallRequest) GetFunctionName() string { + if x != nil { + return x.FunctionName + } + return "" +} + +func (x *FunctionCallRequest) GetParameters() []byte { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *FunctionCallRequest) GetCallId() string { + if x != nil { + return x.CallId + } + return "" +} + +// 函数调用响应 - 从B客户端返回给A客户端 +type FunctionCallResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CallId string `protobuf:"bytes,1,opt,name=call_id,json=callId,proto3" json:"call_id,omitempty"` // 对应请求的调用ID + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` // 调用是否成功 + Result []byte `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty"` // 序列化后的返回结果 + ErrorMessage string `protobuf:"bytes,4,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` // 错误消息(如果失败) +} + +func (x *FunctionCallResponse) Reset() { + *x = FunctionCallResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FunctionCallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionCallResponse) ProtoMessage() {} + +func (x *FunctionCallResponse) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_proto_msgTypes[5] + 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 FunctionCallResponse.ProtoReflect.Descriptor instead. +func (*FunctionCallResponse) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{5} +} + +func (x *FunctionCallResponse) GetCallId() string { + if x != nil { + return x.CallId + } + return "" +} + +func (x *FunctionCallResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *FunctionCallResponse) GetResult() []byte { + if x != nil { + return x.Result + } + return nil +} + +func (x *FunctionCallResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +// 通用消息 +type GenericMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Payload: + // + // *GenericMessage_RegisterRequest + // *GenericMessage_RegisterResponse + // *GenericMessage_FunctionCallRequest + // *GenericMessage_FunctionCallResponse + // *GenericMessage_TextMessage + // *GenericMessage_KickResponse + // *GenericMessage_Sucess + Payload isGenericMessage_Payload `protobuf_oneof:"payload"` +} + +func (x *GenericMessage) Reset() { + *x = GenericMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_manifest_proto_rpc_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GenericMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenericMessage) ProtoMessage() {} + +func (x *GenericMessage) ProtoReflect() protoreflect.Message { + mi := &file_manifest_proto_rpc_proto_msgTypes[6] + 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 GenericMessage.ProtoReflect.Descriptor instead. +func (*GenericMessage) Descriptor() ([]byte, []int) { + return file_manifest_proto_rpc_proto_rawDescGZIP(), []int{6} +} + +func (m *GenericMessage) GetPayload() isGenericMessage_Payload { + if m != nil { + return m.Payload + } + return nil +} + +func (x *GenericMessage) GetRegisterRequest() *RegisterUser { + if x, ok := x.GetPayload().(*GenericMessage_RegisterRequest); ok { + return x.RegisterRequest + } + return nil +} + +func (x *GenericMessage) GetRegisterResponse() *RegisterResponse { + if x, ok := x.GetPayload().(*GenericMessage_RegisterResponse); ok { + return x.RegisterResponse + } + return nil +} + +func (x *GenericMessage) GetFunctionCallRequest() *FunctionCallRequest { + if x, ok := x.GetPayload().(*GenericMessage_FunctionCallRequest); ok { + return x.FunctionCallRequest + } + return nil +} + +func (x *GenericMessage) GetFunctionCallResponse() *FunctionCallResponse { + if x, ok := x.GetPayload().(*GenericMessage_FunctionCallResponse); ok { + return x.FunctionCallResponse + } + return nil +} + +func (x *GenericMessage) GetTextMessage() string { + if x, ok := x.GetPayload().(*GenericMessage_TextMessage); ok { + return x.TextMessage + } + return "" +} + +func (x *GenericMessage) GetKickResponse() *KickRequest { + if x, ok := x.GetPayload().(*GenericMessage_KickResponse); ok { + return x.KickResponse + } + return nil +} + +func (x *GenericMessage) GetSucess() bool { + if x, ok := x.GetPayload().(*GenericMessage_Sucess); ok { + return x.Sucess + } + return false +} + +type isGenericMessage_Payload interface { + isGenericMessage_Payload() +} + +type GenericMessage_RegisterRequest struct { + RegisterRequest *RegisterUser `protobuf:"bytes,1,opt,name=register_request,json=registerRequest,proto3,oneof"` +} + +type GenericMessage_RegisterResponse struct { + RegisterResponse *RegisterResponse `protobuf:"bytes,2,opt,name=register_response,json=registerResponse,proto3,oneof"` +} + +type GenericMessage_FunctionCallRequest struct { + FunctionCallRequest *FunctionCallRequest `protobuf:"bytes,3,opt,name=function_call_request,json=functionCallRequest,proto3,oneof"` +} + +type GenericMessage_FunctionCallResponse struct { + FunctionCallResponse *FunctionCallResponse `protobuf:"bytes,4,opt,name=function_call_response,json=functionCallResponse,proto3,oneof"` +} + +type GenericMessage_TextMessage struct { + TextMessage string `protobuf:"bytes,5,opt,name=text_message,json=textMessage,proto3,oneof"` // 普通文本消息 +} + +type GenericMessage_KickResponse struct { + KickResponse *KickRequest `protobuf:"bytes,6,opt,name=kick_response,json=kickResponse,proto3,oneof"` +} + +type GenericMessage_Sucess struct { + Sucess bool `protobuf:"varint,7,opt,name=sucess,proto3,oneof"` +} + +func (*GenericMessage_RegisterRequest) isGenericMessage_Payload() {} + +func (*GenericMessage_RegisterResponse) isGenericMessage_Payload() {} + +func (*GenericMessage_FunctionCallRequest) isGenericMessage_Payload() {} + +func (*GenericMessage_FunctionCallResponse) isGenericMessage_Payload() {} + +func (*GenericMessage_TextMessage) isGenericMessage_Payload() {} + +func (*GenericMessage_KickResponse) isGenericMessage_Payload() {} + +func (*GenericMessage_Sucess) isGenericMessage_Payload() {} + +var File_manifest_proto_rpc_proto protoreflect.FileDescriptor + +var file_manifest_proto_rpc_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, + 0x43, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x1a, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x22, 0x26, 0x0a, 0x0b, 0x4b, 0x69, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x9b, 0x01, 0x0a, + 0x12, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x70, 0x75, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6f, 0x0a, 0x10, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x9d, 0x01, 0x0a, 0x13, + 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, + 0x0d, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x22, 0x86, 0x01, 0x0a, 0x14, + 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, + 0x23, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x22, 0xbc, 0x03, 0x0a, 0x0e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3e, 0x0a, 0x10, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x55, 0x73, 0x65, 0x72, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x11, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x10, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, + 0x15, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x13, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x51, 0x0a, + 0x16, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x14, 0x66, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x23, 0x0a, 0x0c, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x74, 0x65, 0x78, 0x74, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x37, 0x0a, 0x0d, 0x6b, 0x69, 0x63, 0x6b, 0x5f, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x4b, 0x69, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x0c, 0x6b, 0x69, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, + 0x0a, 0x06, 0x73, 0x75, 0x63, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, + 0x52, 0x06, 0x73, 0x75, 0x63, 0x65, 0x73, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x32, 0x4b, 0x0a, 0x13, 0x42, 0x6f, 0x74, 0x68, 0x57, 0x61, 0x79, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x04, 0x43, 0x61, + 0x6c, 0x6c, 0x12, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x69, 0x63, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, + 0x42, 0x0d, 0x5a, 0x0b, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_manifest_proto_rpc_proto_rawDescOnce sync.Once + file_manifest_proto_rpc_proto_rawDescData = file_manifest_proto_rpc_proto_rawDesc +) + +func file_manifest_proto_rpc_proto_rawDescGZIP() []byte { + file_manifest_proto_rpc_proto_rawDescOnce.Do(func() { + file_manifest_proto_rpc_proto_rawDescData = protoimpl.X.CompressGZIP(file_manifest_proto_rpc_proto_rawDescData) + }) + return file_manifest_proto_rpc_proto_rawDescData +} + +var file_manifest_proto_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_manifest_proto_rpc_proto_goTypes = []any{ + (*RegisterUser)(nil), // 0: api.RegisterUser + (*KickRequest)(nil), // 1: api.KickRequest + (*FunctionDescriptor)(nil), // 2: api.FunctionDescriptor + (*RegisterResponse)(nil), // 3: api.RegisterResponse + (*FunctionCallRequest)(nil), // 4: api.FunctionCallRequest + (*FunctionCallResponse)(nil), // 5: api.FunctionCallResponse + (*GenericMessage)(nil), // 6: api.GenericMessage +} +var file_manifest_proto_rpc_proto_depIdxs = []int32{ + 0, // 0: api.GenericMessage.register_request:type_name -> api.RegisterUser + 3, // 1: api.GenericMessage.register_response:type_name -> api.RegisterResponse + 4, // 2: api.GenericMessage.function_call_request:type_name -> api.FunctionCallRequest + 5, // 3: api.GenericMessage.function_call_response:type_name -> api.FunctionCallResponse + 1, // 4: api.GenericMessage.kick_response:type_name -> api.KickRequest + 6, // 5: api.BothWayStreamServer.Call:input_type -> api.GenericMessage + 6, // 6: api.BothWayStreamServer.Call:output_type -> api.GenericMessage + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_manifest_proto_rpc_proto_init() } +func file_manifest_proto_rpc_proto_init() { + if File_manifest_proto_rpc_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_manifest_proto_rpc_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*RegisterUser); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*KickRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*FunctionDescriptor); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*RegisterResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*FunctionCallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*FunctionCallResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_manifest_proto_rpc_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*GenericMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_manifest_proto_rpc_proto_msgTypes[6].OneofWrappers = []any{ + (*GenericMessage_RegisterRequest)(nil), + (*GenericMessage_RegisterResponse)(nil), + (*GenericMessage_FunctionCallRequest)(nil), + (*GenericMessage_FunctionCallResponse)(nil), + (*GenericMessage_TextMessage)(nil), + (*GenericMessage_KickResponse)(nil), + (*GenericMessage_Sucess)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_manifest_proto_rpc_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_manifest_proto_rpc_proto_goTypes, + DependencyIndexes: file_manifest_proto_rpc_proto_depIdxs, + MessageInfos: file_manifest_proto_rpc_proto_msgTypes, + }.Build() + File_manifest_proto_rpc_proto = out.File + file_manifest_proto_rpc_proto_rawDesc = nil + file_manifest_proto_rpc_proto_goTypes = nil + file_manifest_proto_rpc_proto_depIdxs = nil +} diff --git a/common/api/rpc_grpc.pb.go b/common/api/rpc_grpc.pb.go new file mode 100644 index 000000000..30671c37a --- /dev/null +++ b/common/api/rpc_grpc.pb.go @@ -0,0 +1,143 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.20.0--rc1 +// source: manifest/proto/rpc.proto + +package api + +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 + +const ( + BothWayStreamServer_Call_FullMethodName = "/api.BothWayStreamServer/Call" +) + +// BothWayStreamServerClient is the client API for BothWayStreamServer 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 BothWayStreamServerClient interface { + // 双向流连接 - 用于注册和函数调用 + Call(ctx context.Context, opts ...grpc.CallOption) (BothWayStreamServer_CallClient, error) +} + +type bothWayStreamServerClient struct { + cc grpc.ClientConnInterface +} + +func NewBothWayStreamServerClient(cc grpc.ClientConnInterface) BothWayStreamServerClient { + return &bothWayStreamServerClient{cc} +} + +func (c *bothWayStreamServerClient) Call(ctx context.Context, opts ...grpc.CallOption) (BothWayStreamServer_CallClient, error) { + stream, err := c.cc.NewStream(ctx, &BothWayStreamServer_ServiceDesc.Streams[0], BothWayStreamServer_Call_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &bothWayStreamServerCallClient{stream} + return x, nil +} + +type BothWayStreamServer_CallClient interface { + Send(*GenericMessage) error + Recv() (*GenericMessage, error) + grpc.ClientStream +} + +type bothWayStreamServerCallClient struct { + grpc.ClientStream +} + +func (x *bothWayStreamServerCallClient) Send(m *GenericMessage) error { + return x.ClientStream.SendMsg(m) +} + +func (x *bothWayStreamServerCallClient) Recv() (*GenericMessage, error) { + m := new(GenericMessage) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// BothWayStreamServerServer is the server API for BothWayStreamServer service. +// All implementations must embed UnimplementedBothWayStreamServerServer +// for forward compatibility +type BothWayStreamServerServer interface { + // 双向流连接 - 用于注册和函数调用 + Call(BothWayStreamServer_CallServer) error + mustEmbedUnimplementedBothWayStreamServerServer() +} + +// UnimplementedBothWayStreamServerServer must be embedded to have forward compatible implementations. +type UnimplementedBothWayStreamServerServer struct { +} + +func (UnimplementedBothWayStreamServerServer) Call(BothWayStreamServer_CallServer) error { + return status.Errorf(codes.Unimplemented, "method Call not implemented") +} +func (UnimplementedBothWayStreamServerServer) mustEmbedUnimplementedBothWayStreamServerServer() {} + +// UnsafeBothWayStreamServerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to BothWayStreamServerServer will +// result in compilation errors. +type UnsafeBothWayStreamServerServer interface { + mustEmbedUnimplementedBothWayStreamServerServer() +} + +func RegisterBothWayStreamServerServer(s grpc.ServiceRegistrar, srv BothWayStreamServerServer) { + s.RegisterService(&BothWayStreamServer_ServiceDesc, srv) +} + +func _BothWayStreamServer_Call_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(BothWayStreamServerServer).Call(&bothWayStreamServerCallServer{stream}) +} + +type BothWayStreamServer_CallServer interface { + Send(*GenericMessage) error + Recv() (*GenericMessage, error) + grpc.ServerStream +} + +type bothWayStreamServerCallServer struct { + grpc.ServerStream +} + +func (x *bothWayStreamServerCallServer) Send(m *GenericMessage) error { + return x.ServerStream.SendMsg(m) +} + +func (x *bothWayStreamServerCallServer) Recv() (*GenericMessage, error) { + m := new(GenericMessage) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// BothWayStreamServer_ServiceDesc is the grpc.ServiceDesc for BothWayStreamServer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var BothWayStreamServer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.BothWayStreamServer", + HandlerType: (*BothWayStreamServerServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Call", + Handler: _BothWayStreamServer_Call_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "manifest/proto/rpc.proto", +} diff --git a/common/go.mod b/common/go.mod index ee900414b..1d67e2b26 100644 --- a/common/go.mod +++ b/common/go.mod @@ -12,8 +12,8 @@ require ( require ( go.opentelemetry.io/otel v1.20.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 go.opentelemetry.io/otel/sdk v1.20.0 ) @@ -26,8 +26,13 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/pointernil/bitset32 v0.0.1 // indirect github.com/yitter/idgenerator-go v1.3.3 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) require ( @@ -36,7 +41,7 @@ require ( github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 // indirect @@ -52,9 +57,7 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/tnnmigga/enum v1.0.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect - go.opentelemetry.io/otel/sdk v1.14.0 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.20.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.33.0 // indirect diff --git a/common/go.sum b/common/go.sum index a6d345b37..d4172238f 100644 --- a/common/go.sum +++ b/common/go.sum @@ -1,5 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ECUST-XX/xml v1.20.2 h1:xqg5JaYfcGtkXtLcAN0H1sbTWwRdIHjmU/ZRPmtj+8k= github.com/ECUST-XX/xml v1.20.2/go.mod h1:AHwv/5bl6dD2mohWd7efbLVKEF+SllOsrynpQVhWM0o= github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg= @@ -13,30 +14,42 @@ github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.6.3/go.mod h1:2+evGu1xAlamaYuDdSqa7QCiwPTm1RrGsUFSMc8PyLc= github.com/gogf/gf/v2 v2.6.3 h1:DoqeuwU98wotpFoDSQEx8RZbmJdK8KdGiJtzJeqpyIo= github.com/gogf/gf/v2 v2.6.3/go.mod h1:x2XONYcI4hRQ/4gMNbWHmZrNzSEIg20s2NULbzom5k0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -45,14 +58,17 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 h1:GxMuVb9tJajC1QpbQwYNY1ZAo1EIE8I+UclBjOfjz/M= github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/panjf2000/ants/v2 v2.4.7/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= @@ -83,17 +99,24 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0 h1:CsBiKCiQPdSjS+MlRiqeTI9JDDpSuk0Hb6QTRfwer8k= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0/go.mod h1:CMJYNAfooOwSZSAmAeMUV1M+TXld3BiK++z9fqIm2xk= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= @@ -134,6 +157,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= @@ -145,6 +169,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -164,6 +189,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -183,6 +210,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= @@ -196,13 +224,28 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -216,3 +259,4 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/common/serialize/go-jsonrpc/.github/workflows/go-check.yml b/common/serialize/go-jsonrpc/.github/workflows/go-check.yml new file mode 100644 index 000000000..232775daf --- /dev/null +++ b/common/serialize/go-jsonrpc/.github/workflows/go-check.yml @@ -0,0 +1,18 @@ +name: Go Checks + +on: + pull_request: + push: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: true + +jobs: + go-check: + uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0 diff --git a/common/serialize/go-jsonrpc/.github/workflows/go-test.yml b/common/serialize/go-jsonrpc/.github/workflows/go-test.yml new file mode 100644 index 000000000..3857a2a7e --- /dev/null +++ b/common/serialize/go-jsonrpc/.github/workflows/go-test.yml @@ -0,0 +1,20 @@ +name: Go Test + +on: + pull_request: + push: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: true + +jobs: + go-test: + uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v1.0 + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/common/serialize/go-jsonrpc/.github/workflows/release-check.yml b/common/serialize/go-jsonrpc/.github/workflows/release-check.yml new file mode 100644 index 000000000..0b5ff6070 --- /dev/null +++ b/common/serialize/go-jsonrpc/.github/workflows/release-check.yml @@ -0,0 +1,19 @@ +name: Release Checker + +on: + pull_request_target: + paths: [ 'version.json' ] + types: [ opened, synchronize, reopened, labeled, unlabeled ] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + release-check: + uses: ipdxco/unified-github-workflows/.github/workflows/release-check.yml@v1.0 diff --git a/common/serialize/go-jsonrpc/.github/workflows/releaser.yml b/common/serialize/go-jsonrpc/.github/workflows/releaser.yml new file mode 100644 index 000000000..2ebdbed31 --- /dev/null +++ b/common/serialize/go-jsonrpc/.github/workflows/releaser.yml @@ -0,0 +1,17 @@ +name: Releaser + +on: + push: + paths: [ 'version.json' ] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + +jobs: + releaser: + uses: ipdxco/unified-github-workflows/.github/workflows/releaser.yml@v1.0 diff --git a/common/serialize/go-jsonrpc/.github/workflows/tagpush.yml b/common/serialize/go-jsonrpc/.github/workflows/tagpush.yml new file mode 100644 index 000000000..5ef3fb9ed --- /dev/null +++ b/common/serialize/go-jsonrpc/.github/workflows/tagpush.yml @@ -0,0 +1,18 @@ +name: Tag Push Checker + +on: + push: + tags: + - v* + +permissions: + contents: read + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + releaser: + uses: ipdxco/unified-github-workflows/.github/workflows/tagpush.yml@v1.0 diff --git a/common/serialize/go-jsonrpc/LICENSE-APACHE b/common/serialize/go-jsonrpc/LICENSE-APACHE new file mode 100644 index 000000000..14478a3b6 --- /dev/null +++ b/common/serialize/go-jsonrpc/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/common/serialize/go-jsonrpc/LICENSE-MIT b/common/serialize/go-jsonrpc/LICENSE-MIT new file mode 100644 index 000000000..72dc60d84 --- /dev/null +++ b/common/serialize/go-jsonrpc/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/common/serialize/go-jsonrpc/README.md b/common/serialize/go-jsonrpc/README.md new file mode 100644 index 000000000..03eff66a2 --- /dev/null +++ b/common/serialize/go-jsonrpc/README.md @@ -0,0 +1,313 @@ +go-jsonrpc +================== + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/filecoin-project/go-jsonrpc) +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](https://protocol.ai) + +> Low Boilerplate JSON-RPC 2.0 library + +## Usage examples + +### Server + +```go +// Have a type with some exported methods +type SimpleServerHandler struct { + n int +} + +func (h *SimpleServerHandler) AddGet(in int) int { + h.n += in + return h.n +} + +func main() { + // create a new server instance + rpcServer := jsonrpc.NewServer() + + // create a handler instance and register it + serverHandler := &SimpleServerHandler{} + rpcServer.Register("SimpleServerHandler", serverHandler) + + // rpcServer is now http.Handler which will serve jsonrpc calls to SimpleServerHandler.AddGet + // a method with a single int param, and an int response. The server supports both http and websockets. + + // serve the api + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + fmt.Println("URL: ", "ws://"+testServ.Listener.Addr().String()) + + [..do other app stuff / wait..] +} +``` + +### Client +```go +func start() error { + // Create a struct where each field is an exported function with signatures matching rpc calls + var client struct { + AddGet func(int) int + } + + // Make jsonrp populate func fields in the struct with JSONRPC calls + closer, err := jsonrpc.NewClient(context.Background(), rpcURL, "SimpleServerHandler", &client, nil) + if err != nil { + return err + } + defer closer() + + ... + + n := client.AddGet(10) + // if the server is the one from the example above, n = 10 + + n := client.AddGet(2) + // if the server is the one from the example above, n = 12 +} +``` + +### Supported function signatures + +```go +type _ interface { + // No Params / Return val + Func1() + + // With Params + // Note: If param types implement json.[Un]Marshaler, go-jsonrpc will use it + Func2(param1 int, param2 string, param3 struct{A int}) + + // Returning errors + // * For some connection errors, go-jsonrpc will return jsonrpc.RPCConnectionError{}. + // * RPC-returned errors will be constructed with basic errors.New(__"string message"__) + // * JSON-RPC error codes can be mapped to typed errors with jsonrpc.Errors - https://pkg.go.dev/github.com/filecoin-project/go-jsonrpc#Errors + // * For typed errors to work, server needs to be constructed with the `WithServerErrors` + // option, and the client needs to be constructed with the `WithErrors` option + Func3() error + + // Returning a value + // Note: The value must be serializable with encoding/json. + Func4() int + + // Returning a value and an error + // Note: if the handler returns an error and a non-zero value, the value will not + // be returned to the client - the client will see a zero value. + Func4() (int, error) + + // With context + // * Context isn't passed as JSONRPC param, instead it has a number of different uses + // * When the context is cancelled on the client side, context cancellation should propagate to the server handler + // * In http mode the http request will be aborted + // * In websocket mode the client will send a `xrpc.cancel` with a single param containing ID of the cancelled request + // * If the context contains an opencensus trace span, it will be propagated to the server through a + // `"Meta": {"SpanContext": base64.StdEncoding.EncodeToString(propagation.Binary(span.SpanContext()))}` field in + // the jsonrpc request + // + Func5(ctx context.Context, param1 string) error + + // With non-json-serializable (e.g. interface) params + // * There are client and server options which make it possible to register transformers for types + // to make them json-(de)serializable + // * Server side: jsonrpc.WithParamDecoder(new(io.Reader), func(ctx context.Context, b []byte) (reflect.Value, error) { ... } + // * Client side: jsonrpc.WithParamEncoder(new(io.Reader), func(value reflect.Value) (reflect.Value, error) { ... } + // * For io.Reader specifically there's a simple param encoder/decoder implementation in go-jsonrpc/httpio package + // which will pass reader data through separate http streams on a different hanhler. + // * Note: a similar mechanism for return value transformation isn't supported yet + Func6(r io.Reader) + + // Returning a channel + // * Only supported in websocket mode + // * If no error is returned, the return value will be an int channelId + // * When the server handler writes values into the channel, the client will receive `xrpc.ch.val` notifications + // with 2 params: [chanID: int, value: any] + // * When the channel is closed the client will receive `xrpc.ch.close` notification with a single param: [chanId: int] + // * The client-side channel will be closed when the websocket connection breaks; Server side will discard writes to + // the channel. Handlers should rely on the context to know when to stop writing to the returned channel. + // NOTE: There is no good backpressure mechanism implemented for channels, returning values faster that the client can + // receive them may cause memory leaks. + Func7(ctx context.Context, param1 int, param2 string) (<-chan int, error) +} + +``` + +### Custom Transport Feature +The go-jsonrpc library supports creating clients with custom transport mechanisms (e.g. use for IPC). This allows for greater flexibility in how requests are sent and received, enabling the use of custom protocols, special handling of requests, or integration with other systems. + +#### Example Usage of Custom Transport + +Here is an example demonstrating how to create a custom client with a custom transport mechanism: + +```go +// Setup server +serverHandler := &SimpleServerHandler{} // some type with methods + +rpcServer := jsonrpc.NewServer() +rpcServer.Register("SimpleServerHandler", serverHandler) + +// Custom doRequest function +doRequest := func(ctx context.Context, body []byte) (io.ReadCloser, error) { + reader := bytes.NewReader(body) + pr, pw := io.Pipe() + go func() { + defer pw.Close() + rpcServer.HandleRequest(ctx, reader, pw) // handle the rpc frame + }() + return pr, nil +} + +var client struct { + Add func(int) error +} + +// Create custom client +closer, err := jsonrpc.NewCustomClient("SimpleServerHandler", []interface{}{&client}, doRequest) +if err != nil { + log.Fatalf("Failed to create client: %v", err) +} +defer closer() + +// Use the client +if err := client.Add(10); err != nil { + log.Fatalf("Failed to call Add: %v", err) +} +fmt.Printf("Current value: %d\n", client.AddGet(5)) +``` + +### Reverse Calling Feature +The go-jsonrpc library also supports reverse calling, where the server can make calls to the client. This is useful in scenarios where the server needs to notify or request data from the client. + +NOTE: Reverse calling only works in websocket mode + +#### Example Usage of Reverse Calling + +Here is an example demonstrating how to set up reverse calling: + +```go +// Define the client handler interface +type ClientHandler struct { + CallOnClient func(int) (int, error) +} + +// Define the server handler +type ServerHandler struct {} + +func (h *ServerHandler) Call(ctx context.Context) error { + revClient, ok := jsonrpc.ExtractReverseClient[ClientHandler](ctx) + if !ok { + return fmt.Errorf("no reverse client") + } + + result, err := revClient.CallOnClient(7) // Multiply by 2 on client + if err != nil { + return fmt.Errorf("call on client: %w", err) + } + + if result != 14 { + return fmt.Errorf("unexpected result: %d", result) + } + + return nil +} + +// Define client handler +type RevCallTestClientHandler struct { +} + +func (h *RevCallTestClientHandler) CallOnClient(a int) (int, error) { + return a * 2, nil +} + +// Setup server with reverse client capability +rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[ClientHandler]("Client")) +rpcServer.Register("ServerHandler", &ServerHandler{}) + +testServ := httptest.NewServer(rpcServer) +defer testServ.Close() + +// Setup client with reverse call handler +var client struct { + Call func() error +} + +closer, err := jsonrpc.NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ServerHandler", []interface{}{ + &client, +}, nil, jsonrpc.WithClientHandler("Client", &RevCallTestClientHandler{})) +if err != nil { + log.Fatalf("Failed to create client: %v", err) +} +defer closer() + +// Make a call from the client to the server, which will trigger a reverse call +if err := client.Call(); err != nil { + log.Fatalf("Failed to call server: %v", err) +} +``` + +## Options + +### Using `WithServerMethodNameFormatter` + +`WithServerMethodNameFormatter` allows you to customize a function that formats the JSON-RPC method name, given namespace and method name. + +There are four possible options: +- `jsonrpc.DefaultMethodNameFormatter` - default method name formatter, e.g. `SimpleServerHandler.AddGet` +- `jsonrpc.NewMethodNameFormatter(true, jsonrpc.LowerFirstCharCase)` - method name formatter with namespace, e.g. `SimpleServerHandler.addGet` +- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.OriginalCase)` - method name formatter without namespace, e.g. `AddGet` +- `jsonrpc.NewMethodNameFormatter(false, jsonrpc.LowerFirstCharCase)` - method name formatter without namespace and with the first char lowercased, e.g. `addGet` + +> [!NOTE] +> The default method name formatter concatenates the namespace and method name with a dot. +> Go exported methods are capitalized, so, the method name will be capitalized as well. +> e.g. `SimpleServerHandler.AddGet` (capital "A" in "AddGet") + +```go +func main() { + // create a new server instance with a custom separator + rpcServer := jsonrpc.NewServer(jsonrpc.WithServerMethodNameFormatter( + func(namespace, method string) string { + return namespace + "_" + method + }), + ) + + // create a handler instance and register it + serverHandler := &SimpleServerHandler{} + rpcServer.Register("SimpleServerHandler", serverHandler) + + // serve the api + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + fmt.Println("URL: ", "ws://"+testServ.Listener.Addr().String()) + + // rpc method becomes SimpleServerHandler_AddGet + + [..do other app stuff / wait..] +} +``` + +### Using `WithMethodNameFormatter` + +`WithMethodNameFormatter` is the client-side counterpart to `WithServerMethodNameFormatter`. + +```go +func main() { + closer, err := NewMergeClient( + context.Background(), + "http://example.com", + "SimpleServerHandler", + []any{&client}, + nil, + WithMethodNameFormatter(jsonrpc.NewMethodNameFormatter(false, OriginalCase)), + ) + defer closer() +} +``` + +## Contribute + +PRs are welcome! + +## License + +Dual-licensed under [MIT](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-MIT) + [Apache 2.0](https://github.com/filecoin-project/go-jsonrpc/blob/master/LICENSE-APACHE) diff --git a/common/serialize/go-jsonrpc/auth/auth.go b/common/serialize/go-jsonrpc/auth/auth.go new file mode 100644 index 000000000..d0b07ed37 --- /dev/null +++ b/common/serialize/go-jsonrpc/auth/auth.go @@ -0,0 +1,79 @@ +package auth + +import ( + "context" + "reflect" + + "golang.org/x/xerrors" +) + +type Permission string + +type permKey int + +var permCtxKey permKey + +func WithPerm(ctx context.Context, perms []Permission) context.Context { + return context.WithValue(ctx, permCtxKey, perms) +} + +func HasPerm(ctx context.Context, defaultPerms []Permission, perm Permission) bool { + callerPerms, ok := ctx.Value(permCtxKey).([]Permission) + if !ok { + callerPerms = defaultPerms + } + + for _, callerPerm := range callerPerms { + if callerPerm == perm { + return true + } + } + return false +} + +func PermissionedProxy(validPerms, defaultPerms []Permission, in interface{}, out interface{}) { + rint := reflect.ValueOf(out).Elem() + ra := reflect.ValueOf(in) + + for f := 0; f < rint.NumField(); f++ { + field := rint.Type().Field(f) + requiredPerm := Permission(field.Tag.Get("perm")) + if requiredPerm == "" { + panic("missing 'perm' tag on " + field.Name) // ok + } + + // Validate perm tag + ok := false + for _, perm := range validPerms { + if requiredPerm == perm { + ok = true + break + } + } + if !ok { + panic("unknown 'perm' tag on " + field.Name) // ok + } + + fn := ra.MethodByName(field.Name) + + rint.Field(f).Set(reflect.MakeFunc(field.Type, func(args []reflect.Value) (results []reflect.Value) { + ctx := args[0].Interface().(context.Context) + if HasPerm(ctx, defaultPerms, requiredPerm) { + return fn.Call(args) + } + + err := xerrors.Errorf("missing permission to invoke '%s' (need '%s')", field.Name, requiredPerm) + rerr := reflect.ValueOf(&err).Elem() + + if field.Type.NumOut() == 2 { + return []reflect.Value{ + reflect.Zero(field.Type.Out(0)), + rerr, + } + } else { + return []reflect.Value{rerr} + } + })) + + } +} diff --git a/common/serialize/go-jsonrpc/auth/handler.go b/common/serialize/go-jsonrpc/auth/handler.go new file mode 100644 index 000000000..63a1c60f9 --- /dev/null +++ b/common/serialize/go-jsonrpc/auth/handler.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "net/http" + "strings" + + logging "github.com/ipfs/go-log/v2" +) + +var log = logging.Logger("auth") + +type Handler struct { + Verify func(ctx context.Context, token string) ([]Permission, error) + Next http.HandlerFunc +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + token := r.Header.Get("Authorization") + if token == "" { + token = r.FormValue("token") + if token != "" { + token = "Bearer " + token + } + } + + if token != "" { + if !strings.HasPrefix(token, "Bearer ") { + log.Warn("missing Bearer prefix in auth header") + w.WriteHeader(401) + return + } + token = strings.TrimPrefix(token, "Bearer ") + + allow, err := h.Verify(ctx, token) + if err != nil { + log.Warnf("JWT Verification failed (originating from %s): %s", r.RemoteAddr, err) + w.WriteHeader(401) + return + } + + ctx = WithPerm(ctx, allow) + } + + h.Next(w, r.WithContext(ctx)) +} diff --git a/common/serialize/go-jsonrpc/client.go b/common/serialize/go-jsonrpc/client.go new file mode 100644 index 000000000..69a9cfecf --- /dev/null +++ b/common/serialize/go-jsonrpc/client.go @@ -0,0 +1,750 @@ +package jsonrpc + +import ( + "bytes" + "container/list" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "reflect" + "runtime/pprof" + "sync/atomic" + "time" + + "github.com/google/uuid" + "github.com/gorilla/websocket" + logging "github.com/ipfs/go-log/v2" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" + "golang.org/x/xerrors" +) + +const ( + methodMinRetryDelay = 100 * time.Millisecond + methodMaxRetryDelay = 10 * time.Minute +) + +var ( + errorType = reflect.TypeOf(new(error)).Elem() + contextType = reflect.TypeOf(new(context.Context)).Elem() + + log = logging.Logger("rpc") + + _defaultHTTPClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } +) + +// ErrClient is an error which occurred on the client side the library +type ErrClient struct { + err error +} + +func (e *ErrClient) Error() string { + return fmt.Sprintf("RPC client error: %s", e.err) +} + +// Unwrap unwraps the actual error +func (e *ErrClient) Unwrap() error { + return e.err +} + +type clientResponse struct { + Jsonrpc string `json:"jsonrpc"` + Result json.RawMessage `json:"result"` + ID interface{} `json:"id"` + Error *JSONRPCError `json:"error,omitempty"` +} + +type makeChanSink func() (context.Context, func([]byte, bool)) + +type clientRequest struct { + req request + ready chan clientResponse + + // retCh provides a context and sink for handling incoming channel messages + retCh makeChanSink +} + +// ClientCloser is used to close Client from further use +type ClientCloser func() + +// NewClient creates new jsonrpc 2.0 client +// +// handler must be pointer to a struct with function fields +// Returned value closes the client connection +// TODO: Example +func NewClient(ctx context.Context, addr string, namespace string, handler interface{}, requestHeader http.Header) (ClientCloser, error) { + return NewMergeClient(ctx, addr, namespace, []interface{}{handler}, requestHeader) +} + +type client struct { + namespace string + paramEncoders map[reflect.Type]ParamEncoder + errors *Errors + + doRequest func(context.Context, clientRequest) (clientResponse, error) + exiting <-chan struct{} + idCtr int64 + + methodNameFormatter MethodNameFormatter +} + +// NewMergeClient is like NewClient, but allows to specify multiple structs +// to be filled in the same namespace, using one connection +func NewMergeClient(ctx context.Context, addr string, namespace string, outs []interface{}, requestHeader http.Header, opts ...Option) (ClientCloser, error) { + config := defaultConfig() + for _, o := range opts { + o(&config) + } + + u, err := url.Parse(addr) + if err != nil { + return nil, xerrors.Errorf("parsing address: %w", err) + } + + switch u.Scheme { + case "ws", "wss": + return websocketClient(ctx, addr, namespace, outs, requestHeader, config) + case "http", "https": + return httpClient(ctx, addr, namespace, outs, requestHeader, config) + default: + return nil, xerrors.Errorf("unknown url scheme '%s'", u.Scheme) + } + +} + +// NewCustomClient is like NewMergeClient in single-request (http) mode, except it allows for a custom doRequest function +func NewCustomClient(namespace string, outs []interface{}, doRequest func(ctx context.Context, body []byte) (io.ReadCloser, error), opts ...Option) (ClientCloser, error) { + config := defaultConfig() + for _, o := range opts { + o(&config) + } + + c := client{ + namespace: namespace, + paramEncoders: config.paramEncoders, + errors: config.errors, + methodNameFormatter: config.methodNamer, + } + + stop := make(chan struct{}) + c.exiting = stop + + c.doRequest = func(ctx context.Context, cr clientRequest) (clientResponse, error) { + b, err := json.Marshal(&cr.req) + if err != nil { + return clientResponse{}, xerrors.Errorf("marshalling request: %w", err) + } + + if ctx == nil { + ctx = context.Background() + } + + rawResp, err := doRequest(ctx, b) + if err != nil { + return clientResponse{}, xerrors.Errorf("doRequest failed: %w", err) + } + + defer rawResp.Close() + + var resp clientResponse + if cr.req.ID != nil { // non-notification + if err := json.NewDecoder(rawResp).Decode(&resp); err != nil { + return clientResponse{}, xerrors.Errorf("unmarshaling response: %w", err) + } + + if resp.ID, err = normalizeID(resp.ID); err != nil { + return clientResponse{}, xerrors.Errorf("failed to response ID: %w", err) + } + + if resp.ID != cr.req.ID { + return clientResponse{}, xerrors.New("request and response id didn't match") + } + } + + return resp, nil + } + + if err := c.provide(outs); err != nil { + return nil, err + } + + return func() { + close(stop) + }, nil +} + +func httpClient(ctx context.Context, addr string, namespace string, outs []interface{}, requestHeader http.Header, config Config) (ClientCloser, error) { + c := client{ + namespace: namespace, + paramEncoders: config.paramEncoders, + errors: config.errors, + methodNameFormatter: config.methodNamer, + } + + stop := make(chan struct{}) + c.exiting = stop + + if requestHeader == nil { + requestHeader = http.Header{} + } + + c.doRequest = func(ctx context.Context, cr clientRequest) (clientResponse, error) { + b, err := json.Marshal(&cr.req) + if err != nil { + return clientResponse{}, xerrors.Errorf("marshalling request: %w", err) + } + + hreq, err := http.NewRequest("POST", addr, bytes.NewReader(b)) + if err != nil { + return clientResponse{}, &RPCConnectionError{err} + } + + hreq.Header = requestHeader.Clone() + + if ctx != nil { + hreq = hreq.WithContext(ctx) + } + + hreq.Header.Set("Content-Type", "application/json") + + httpResp, err := config.httpClient.Do(hreq) + if err != nil { + return clientResponse{}, &RPCConnectionError{err} + } + + // likely a failure outside of our control and ability to inspect; jsonrpc server only ever + // returns json format errors with either a StatusBadRequest or a StatusInternalServerError + if httpResp.StatusCode > http.StatusBadRequest && httpResp.StatusCode != http.StatusInternalServerError { + return clientResponse{}, xerrors.Errorf("request failed, http status %s", httpResp.Status) + } + + defer httpResp.Body.Close() + + var resp clientResponse + if cr.req.ID != nil { // non-notification + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + return clientResponse{}, xerrors.Errorf("http status %s unmarshaling response: %w", httpResp.Status, err) + } + + if resp.ID, err = normalizeID(resp.ID); err != nil { + return clientResponse{}, xerrors.Errorf("failed to response ID: %w", err) + } + + if resp.ID != cr.req.ID { + return clientResponse{}, xerrors.New("request and response id didn't match") + } + } + + return resp, nil + } + + if err := c.provide(outs); err != nil { + return nil, err + } + + return func() { + close(stop) + }, nil +} + +func websocketClient(ctx context.Context, addr string, namespace string, outs []interface{}, requestHeader http.Header, config Config) (ClientCloser, error) { + connFactory := func() (*websocket.Conn, error) { + conn, _, err := websocket.DefaultDialer.Dial(addr, requestHeader) + if err != nil { + return nil, &RPCConnectionError{xerrors.Errorf("cannot dial address %s for %w", addr, err)} + } + return conn, nil + } + + if config.proxyConnFactory != nil { + // used in tests + connFactory = config.proxyConnFactory(connFactory) + } + + conn, err := connFactory() + if err != nil { + return nil, err + } + + if config.noReconnect { + connFactory = nil + } + + c := client{ + namespace: namespace, + paramEncoders: config.paramEncoders, + errors: config.errors, + methodNameFormatter: config.methodNamer, + } + + requests := c.setupRequestChan() + + stop := make(chan struct{}) + exiting := make(chan struct{}) + c.exiting = exiting + + var hnd reqestHandler + if len(config.reverseHandlers) > 0 { + h := makeHandler(defaultServerConfig()) + h.aliasedMethods = config.aliasedHandlerMethods + for _, reverseHandler := range config.reverseHandlers { + h.register(reverseHandler.ns, reverseHandler.hnd) + } + hnd = h + } + + wconn := &wsConn{ + conn: conn, + connFactory: connFactory, + reconnectBackoff: config.reconnectBackoff, + pingInterval: config.pingInterval, + timeout: config.timeout, + handler: hnd, + requests: requests, + stop: stop, + exiting: exiting, + } + + go func() { + lbl := pprof.Labels("jrpc-mode", "wsclient", "jrpc-remote", addr, "jrpc-local", conn.LocalAddr().String(), "jrpc-uuid", uuid.New().String()) + pprof.Do(ctx, lbl, func(ctx context.Context) { + wconn.handleWsConn(ctx) + }) + }() + + if err := c.provide(outs); err != nil { + return nil, err + } + + return func() { + close(stop) + <-exiting + }, nil +} + +func (c *client) setupRequestChan() chan clientRequest { + requests := make(chan clientRequest) + + c.doRequest = func(ctx context.Context, cr clientRequest) (clientResponse, error) { + select { + case requests <- cr: + case <-c.exiting: + return clientResponse{}, fmt.Errorf("websocket routine exiting") + } + + var ctxDone <-chan struct{} + var resp clientResponse + + if ctx != nil { + ctxDone = ctx.Done() + } + + // wait for response, handle context cancellation + loop: + for { + select { + case resp = <-cr.ready: + break loop + case <-ctxDone: // send cancel request + ctxDone = nil + + rp, err := json.Marshal([]param{{v: reflect.ValueOf(cr.req.ID)}}) + if err != nil { + return clientResponse{}, xerrors.Errorf("marshalling cancel request: %w", err) + } + + cancelReq := clientRequest{ + req: request{ + Jsonrpc: "2.0", + Method: wsCancel, + Params: rp, + }, + ready: make(chan clientResponse, 1), + } + select { + case requests <- cancelReq: + case <-c.exiting: + log.Warn("failed to send request cancellation, websocket routing exited") + } + + } + } + + return resp, nil + } + + return requests +} + +func (c *client) provide(outs []interface{}) error { + for _, handler := range outs { + htyp := reflect.TypeOf(handler) + if htyp.Kind() != reflect.Ptr { + return xerrors.New("expected handler to be a pointer") + } + typ := htyp.Elem() + if typ.Kind() != reflect.Struct { + return xerrors.New("handler should be a struct") + } + + val := reflect.ValueOf(handler) + + for i := 0; i < typ.NumField(); i++ { + fn, err := c.makeRpcFunc(typ.Field(i)) + if err != nil { + return err + } + + val.Elem().Field(i).Set(fn) + } + } + + return nil +} + +func (c *client) makeOutChan(ctx context.Context, ftyp reflect.Type, valOut int) (func() reflect.Value, makeChanSink) { + retVal := reflect.Zero(ftyp.Out(valOut)) + + chCtor := func() (context.Context, func([]byte, bool)) { + // unpack chan type to make sure it's reflect.BothDir + ctyp := reflect.ChanOf(reflect.BothDir, ftyp.Out(valOut).Elem()) + ch := reflect.MakeChan(ctyp, 0) // todo: buffer? + retVal = ch.Convert(ftyp.Out(valOut)) + + incoming := make(chan reflect.Value, 32) + + // gorotuine to handle buffering of items + go func() { + buf := (&list.List{}).Init() + + for { + front := buf.Front() + + cases := []reflect.SelectCase{ + { + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ctx.Done()), + }, + { + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(incoming), + }, + } + + if front != nil { + cases = append(cases, reflect.SelectCase{ + Dir: reflect.SelectSend, + Chan: ch, + Send: front.Value.(reflect.Value).Elem(), + }) + } + + chosen, val, ok := reflect.Select(cases) + + switch chosen { + case 0: + ch.Close() + return + case 1: + if ok { + vvval := val.Interface().(reflect.Value) + buf.PushBack(vvval) + if buf.Len() > 1 { + if buf.Len() > 10 { + log.Warnw("rpc output message buffer", "n", buf.Len()) + } else { + log.Debugw("rpc output message buffer", "n", buf.Len()) + } + } + } else { + incoming = nil + } + + case 2: + buf.Remove(front) + } + + if incoming == nil && buf.Len() == 0 { + ch.Close() + return + } + } + }() + + return ctx, func(result []byte, ok bool) { + if !ok { + close(incoming) + return + } + + val := reflect.New(ftyp.Out(valOut).Elem()) + if err := json.Unmarshal(result, val.Interface()); err != nil { + log.Errorf("error unmarshaling chan response: %s", err) + return + } + + if ctx.Err() != nil { + log.Errorf("got rpc message with cancelled context: %s", ctx.Err()) + return + } + + select { + case incoming <- val: + case <-ctx.Done(): + } + } + } + + return func() reflect.Value { return retVal }, chCtor +} + +func (c *client) sendRequest(ctx context.Context, req request, chCtor makeChanSink) (clientResponse, error) { + creq := clientRequest{ + req: req, + ready: make(chan clientResponse, 1), + + retCh: chCtor, + } + + return c.doRequest(ctx, creq) +} + +type rpcFunc struct { + client *client + + ftyp reflect.Type + name string + + nout int + valOut int + errOut int + + // hasCtx is 1 if the function has a context.Context as its first argument. + // Used as the number of the first non-context argument. + hasCtx int + + hasRawParams bool + returnValueIsChannel bool + + retry bool + notify bool +} + +func (fn *rpcFunc) processResponse(resp clientResponse, rval reflect.Value) []reflect.Value { + out := make([]reflect.Value, fn.nout) + + if fn.valOut != -1 { + out[fn.valOut] = rval + } + if fn.errOut != -1 { + out[fn.errOut] = reflect.New(errorType).Elem() + if resp.Error != nil { + + out[fn.errOut].Set(resp.Error.val(fn.client.errors)) + } + } + + return out +} + +func (fn *rpcFunc) processError(err error) []reflect.Value { + out := make([]reflect.Value, fn.nout) + + if fn.valOut != -1 { + out[fn.valOut] = reflect.New(fn.ftyp.Out(fn.valOut)).Elem() + } + if fn.errOut != -1 { + out[fn.errOut] = reflect.New(errorType).Elem() + out[fn.errOut].Set(reflect.ValueOf(&ErrClient{err})) + } + + return out +} + +func (fn *rpcFunc) handleRpcCall(args []reflect.Value) (results []reflect.Value) { + var id interface{} + if !fn.notify { + id = atomic.AddInt64(&fn.client.idCtr, 1) + + // Prepare the ID to send on the wire. + // We track int64 ids as float64 in the inflight map (because that's what + // they'll be decoded to). encoding/json outputs numbers with their minimal + // encoding, avoding the decimal point when possible, i.e. 3 will never get + // converted to 3.0. + var err error + id, err = normalizeID(id) + if err != nil { + return fn.processError(fmt.Errorf("failed to normalize id")) // should probably panic + } + } + + var serializedParams json.RawMessage + + if fn.hasRawParams { + serializedParams = json.RawMessage(args[fn.hasCtx].Interface().(RawParams)) + } else { + params := make([]param, len(args)-fn.hasCtx) + for i, arg := range args[fn.hasCtx:] { + enc, found := fn.client.paramEncoders[arg.Type()] + if found { + // custom param encoder + var err error + arg, err = enc(arg) + if err != nil { + return fn.processError(fmt.Errorf("sendRequest failed: %w", err)) + } + } + + params[i] = param{ + v: arg, + } + } + var err error + serializedParams, err = json.Marshal(params) + if err != nil { + return fn.processError(fmt.Errorf("marshaling params failed: %w", err)) + } + } + + var ctx context.Context + var span *trace.Span + if fn.hasCtx == 1 { + ctx = args[0].Interface().(context.Context) + ctx, span = trace.StartSpan(ctx, "api.call") + defer span.End() + } + + retVal := func() reflect.Value { return reflect.Value{} } + + // if the function returns a channel, we need to provide a sink for the + // messages + var chCtor makeChanSink + if fn.returnValueIsChannel { + retVal, chCtor = fn.client.makeOutChan(ctx, fn.ftyp, fn.valOut) + } + + req := request{ + Jsonrpc: "2.0", + ID: id, + Method: fn.name, + Params: serializedParams, + } + + if span != nil { + span.AddAttributes(trace.StringAttribute("method", req.Method)) + + eSC := base64.StdEncoding.EncodeToString( + propagation.Binary(span.SpanContext())) + req.Meta = map[string]string{ + "SpanContext": eSC, + } + } + + b := backoff{ + maxDelay: methodMaxRetryDelay, + minDelay: methodMinRetryDelay, + } + + var err error + var resp clientResponse + // keep retrying if got a forced closed websocket conn and calling method + // has retry annotation + for attempt := 0; true; attempt++ { + resp, err = fn.client.sendRequest(ctx, req, chCtor) + if err != nil { + return fn.processError(fmt.Errorf("sendRequest failed: %w", err)) + } + + if !fn.notify && resp.ID != req.ID { + return fn.processError(xerrors.New("request and response id didn't match")) + } + + if fn.valOut != -1 && !fn.returnValueIsChannel { + val := reflect.New(fn.ftyp.Out(fn.valOut)) + + if resp.Result != nil { + log.Debugw("rpc result", "type", fn.ftyp.Out(fn.valOut)) + if err := json.Unmarshal(resp.Result, val.Interface()); err != nil { + log.Warnw("unmarshaling failed", "message", string(resp.Result)) + return fn.processError(xerrors.Errorf("unmarshaling result: %w", err)) + } + } + + retVal = func() reflect.Value { return val.Elem() } + } + retry := resp.Error != nil && resp.Error.Code == eTempWSError && fn.retry + if !retry { + break + } + + time.Sleep(b.next(attempt)) + } + + return fn.processResponse(resp, retVal()) +} + +const ( + ProxyTagRetry = "retry" + ProxyTagNotify = "notify" + ProxyTagRPCMethod = "rpc_method" +) + +func (c *client) makeRpcFunc(f reflect.StructField) (reflect.Value, error) { + ftyp := f.Type + if ftyp.Kind() != reflect.Func { + return reflect.Value{}, xerrors.New("handler field not a func") + } + + name := c.methodNameFormatter(c.namespace, f.Name) + if tag, ok := f.Tag.Lookup(ProxyTagRPCMethod); ok { + name = tag + } + + fun := &rpcFunc{ + client: c, + ftyp: ftyp, + name: name, + retry: f.Tag.Get(ProxyTagRetry) == "true", + notify: f.Tag.Get(ProxyTagNotify) == "true", + } + fun.valOut, fun.errOut, fun.nout = processFuncOut(ftyp) + + if fun.valOut != -1 && fun.notify { + return reflect.Value{}, xerrors.New("notify methods cannot return values") + } + + fun.returnValueIsChannel = fun.valOut != -1 && ftyp.Out(fun.valOut).Kind() == reflect.Chan + + if ftyp.NumIn() > 0 && ftyp.In(0) == contextType { + fun.hasCtx = 1 + } + // note: hasCtx is also the number of the first non-context argument + if ftyp.NumIn() > fun.hasCtx && ftyp.In(fun.hasCtx) == rtRawParams { + if ftyp.NumIn() > fun.hasCtx+1 { + return reflect.Value{}, xerrors.New("raw params can't be mixed with other arguments") + } + fun.hasRawParams = true + } + + return reflect.MakeFunc(ftyp, fun.handleRpcCall), nil +} diff --git a/common/serialize/go-jsonrpc/errors.go b/common/serialize/go-jsonrpc/errors.go new file mode 100644 index 000000000..cf054da02 --- /dev/null +++ b/common/serialize/go-jsonrpc/errors.go @@ -0,0 +1,65 @@ +package jsonrpc + +import ( + "encoding/json" + "errors" + "reflect" +) + +const eTempWSError = -1111111 + +type RPCConnectionError struct { + err error +} + +func (e *RPCConnectionError) Error() string { + if e.err != nil { + return e.err.Error() + } + return "RPCConnectionError" +} + +func (e *RPCConnectionError) Unwrap() error { + if e.err != nil { + return e.err + } + return errors.New("RPCConnectionError") +} + +type Errors struct { + byType map[reflect.Type]ErrorCode + byCode map[ErrorCode]reflect.Type +} + +type ErrorCode int + +const FirstUserCode = 2 + +func NewErrors() Errors { + return Errors{ + byType: map[reflect.Type]ErrorCode{}, + byCode: map[ErrorCode]reflect.Type{ + -1111111: reflect.TypeOf(&RPCConnectionError{}), + }, + } +} + +func (e *Errors) Register(c ErrorCode, typ interface{}) { + rt := reflect.TypeOf(typ).Elem() + if !rt.Implements(errorType) { + panic("can't register non-error types") + } + + e.byType[rt] = c + e.byCode[c] = rt +} + +type marshalable interface { + json.Marshaler + json.Unmarshaler +} + +type RPCErrorCodec interface { + FromJSONRPCError(JSONRPCError) error + ToJSONRPCError() (JSONRPCError, error) +} diff --git a/common/serialize/go-jsonrpc/go.mod b/common/serialize/go-jsonrpc/go.mod new file mode 100644 index 000000000..38c6b68c1 --- /dev/null +++ b/common/serialize/go-jsonrpc/go.mod @@ -0,0 +1,23 @@ +module github.com/filecoin-project/go-jsonrpc + +go 1.20 + +require ( + github.com/google/uuid v1.1.1 + github.com/gorilla/mux v1.7.4 + github.com/gorilla/websocket v1.4.2 + github.com/ipfs/go-log/v2 v2.0.8 + github.com/stretchr/testify v1.5.1 + go.opencensus.io v0.22.3 + go.uber.org/zap v1.14.1 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.6.0 // indirect + go.uber.org/multierr v1.5.0 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/common/serialize/go-jsonrpc/go.sum b/common/serialize/go-jsonrpc/go.sum new file mode 100644 index 000000000..82ff66817 --- /dev/null +++ b/common/serialize/go-jsonrpc/go.sum @@ -0,0 +1,102 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/ipfs/go-log/v2 v2.0.8 h1:3b3YNopMHlj4AvyhWAx0pDxqSQWYi4/WuWO7yRV6/Qg= +github.com/ipfs/go-log/v2 v2.0.8/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/common/serialize/go-jsonrpc/handler.go b/common/serialize/go-jsonrpc/handler.go new file mode 100644 index 000000000..b54b99753 --- /dev/null +++ b/common/serialize/go-jsonrpc/handler.go @@ -0,0 +1,513 @@ +package jsonrpc + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "reflect" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-jsonrpc/metrics" +) + +type RawParams json.RawMessage + +var rtRawParams = reflect.TypeOf(RawParams{}) + +// todo is there a better way to tell 'struct with any number of fields'? +func DecodeParams[T any](p RawParams) (T, error) { + var t T + err := json.Unmarshal(p, &t) + + // todo also handle list-encoding automagically (json.Unmarshal doesn't do that, does it?) + + return t, err +} + +// methodHandler is a handler for a single method +type methodHandler struct { + paramReceivers []reflect.Type + nParams int + + receiver reflect.Value + handlerFunc reflect.Value + + hasCtx int + hasRawParams bool + + errOut int + valOut int +} + +// Request / response + +type request struct { + Jsonrpc string `json:"jsonrpc"` + ID interface{} `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + Meta map[string]string `json:"meta,omitempty"` +} + +// Limit request size. Ideally this limit should be specific for each field +// in the JSON request but as a simple defensive measure we just limit the +// entire HTTP body. +// Configured by WithMaxRequestSize. +const DEFAULT_MAX_REQUEST_SIZE = 100 << 20 // 100 MiB + +type handler struct { + methods map[string]methodHandler + errors *Errors + + maxRequestSize int64 + + // aliasedMethods contains a map of alias:original method names. + // These are used as fallbacks if a method is not found by the given method name. + aliasedMethods map[string]string + + paramDecoders map[reflect.Type]ParamDecoder + + methodNameFormatter MethodNameFormatter + + tracer Tracer +} + +type Tracer func(method string, params []reflect.Value, results []reflect.Value, err error) + +func makeHandler(sc ServerConfig) *handler { + return &handler{ + methods: make(map[string]methodHandler), + errors: sc.errors, + + aliasedMethods: map[string]string{}, + paramDecoders: sc.paramDecoders, + + methodNameFormatter: sc.methodNameFormatter, + + maxRequestSize: sc.maxRequestSize, + + tracer: sc.tracer, + } +} + +// Register + +func (s *handler) register(namespace string, r interface{}) { + val := reflect.ValueOf(r) + // TODO: expect ptr + + for i := 0; i < val.NumMethod(); i++ { + method := val.Type().Method(i) + + funcType := method.Func.Type() + hasCtx := 0 + if funcType.NumIn() >= 2 && funcType.In(1) == contextType { + hasCtx = 1 + } + + hasRawParams := false + ins := funcType.NumIn() - 1 - hasCtx + recvs := make([]reflect.Type, ins) + for i := 0; i < ins; i++ { + if hasRawParams && i > 0 { + panic("raw params must be the last parameter") + } + if funcType.In(i+1+hasCtx) == rtRawParams { + hasRawParams = true + } + recvs[i] = method.Type.In(i + 1 + hasCtx) + } + + valOut, errOut, _ := processFuncOut(funcType) + + s.methods[s.methodNameFormatter(namespace, method.Name)] = methodHandler{ + paramReceivers: recvs, + nParams: ins, + + handlerFunc: method.Func, + receiver: val, + + hasCtx: hasCtx, + hasRawParams: hasRawParams, + + errOut: errOut, + valOut: valOut, + } + } +} + +// Handle + +type rpcErrFunc func(w func(func(io.Writer)), req *request, code ErrorCode, err error) +type chanOut func(reflect.Value, interface{}) error + +func (s *handler) handleReader(ctx context.Context, r io.Reader, w io.Writer, rpcError rpcErrFunc) { + wf := func(cb func(io.Writer)) { + cb(w) + } + + // We read the entire request upfront in a buffer to be able to tell if the + // client sent more than maxRequestSize and report it back as an explicit error, + // instead of just silently truncating it and reporting a more vague parsing + // error. + bufferedRequest := new(bytes.Buffer) + // We use LimitReader to enforce maxRequestSize. Since it won't return an + // EOF we can't actually know if the client sent more than the maximum or + // not, so we read one byte more over the limit to explicitly query that. + // FIXME: Maybe there's a cleaner way to do this. + reqSize, err := bufferedRequest.ReadFrom(io.LimitReader(r, s.maxRequestSize+1)) + if err != nil { + // ReadFrom will discard EOF so any error here is unexpected and should + // be reported. + rpcError(wf, nil, rpcParseError, xerrors.Errorf("reading request: %w", err)) + return + } + if reqSize > s.maxRequestSize { + rpcError(wf, nil, rpcParseError, + // rpcParseError is the closest we have from the standard errors defined + // in [jsonrpc spec](https://www.jsonrpc.org/specification#error_object) + // to report the maximum limit. + xerrors.Errorf("request bigger than maximum %d allowed", + s.maxRequestSize)) + return + } + + // Trim spaces to avoid issues with batch request detection. + bufferedRequest = bytes.NewBuffer(bytes.TrimSpace(bufferedRequest.Bytes())) + reqSize = int64(bufferedRequest.Len()) + + if reqSize == 0 { + rpcError(wf, nil, rpcInvalidRequest, xerrors.New("Invalid request")) + return + } + + if bufferedRequest.Bytes()[0] == '[' && bufferedRequest.Bytes()[reqSize-1] == ']' { + var reqs []request + + if err := json.NewDecoder(bufferedRequest).Decode(&reqs); err != nil { + rpcError(wf, nil, rpcParseError, xerrors.New("Parse error")) + return + } + + if len(reqs) == 0 { + rpcError(wf, nil, rpcInvalidRequest, xerrors.New("Invalid request")) + return + } + + _, _ = w.Write([]byte("[")) // todo consider handling this error + for idx, req := range reqs { + if req.ID, err = normalizeID(req.ID); err != nil { + rpcError(wf, &req, rpcParseError, xerrors.Errorf("failed to parse ID: %w", err)) + return + } + + s.handle(ctx, req, wf, rpcError, func(bool) {}, nil) + + if idx != len(reqs)-1 { + _, _ = w.Write([]byte(",")) // todo consider handling this error + } + } + _, _ = w.Write([]byte("]")) // todo consider handling this error + } else { + var req request + if err := json.NewDecoder(bufferedRequest).Decode(&req); err != nil { + rpcError(wf, &req, rpcParseError, xerrors.New("Parse error")) + return + } + + if req.ID, err = normalizeID(req.ID); err != nil { + rpcError(wf, &req, rpcParseError, xerrors.Errorf("failed to parse ID: %w", err)) + return + } + + s.handle(ctx, req, wf, rpcError, func(bool) {}, nil) + } +} + +func doCall(methodName string, f reflect.Value, params []reflect.Value) (out []reflect.Value, err error) { + defer func() { + if i := recover(); i != nil { + err = xerrors.Errorf("panic in rpc method '%s': %s", methodName, i) + log.Desugar().WithOptions(zap.AddStacktrace(zapcore.ErrorLevel)).Sugar().Error(err) + } + }() + + out = f.Call(params) + return out, nil +} + +func (s *handler) getSpan(ctx context.Context, req request) (context.Context, *trace.Span) { + if req.Meta == nil { + return ctx, nil + } + + var span *trace.Span + if eSC, ok := req.Meta["SpanContext"]; ok { + bSC := make([]byte, base64.StdEncoding.DecodedLen(len(eSC))) + _, err := base64.StdEncoding.Decode(bSC, []byte(eSC)) + if err != nil { + log.Errorf("SpanContext: decode", "error", err) + return ctx, nil + } + sc, ok := propagation.FromBinary(bSC) + if !ok { + log.Errorf("SpanContext: could not create span", "data", bSC) + return ctx, nil + } + ctx, span = trace.StartSpanWithRemoteParent(ctx, "api.handle", sc) + } else { + ctx, span = trace.StartSpan(ctx, "api.handle") + } + + span.AddAttributes(trace.StringAttribute("method", req.Method)) + return ctx, span +} + +func (s *handler) createError(err error) *JSONRPCError { + var code ErrorCode = 1 + if s.errors != nil { + c, ok := s.errors.byType[reflect.TypeOf(err)] + if ok { + code = c + } + } + + out := &JSONRPCError{ + Code: code, + Message: err.Error(), + } + + switch m := err.(type) { + case RPCErrorCodec: + o, err := m.ToJSONRPCError() + if err != nil { + log.Errorf("Failed to convert error to JSONRPCError: %w", err) + } else { + out = &o + } + case marshalable: + meta, marshalErr := m.MarshalJSON() + if marshalErr == nil { + out.Meta = meta + } else { + log.Errorf("Failed to marshal error metadata: %w", marshalErr) + } + } + + return out +} + +func (s *handler) handle(ctx context.Context, req request, w func(func(io.Writer)), rpcError rpcErrFunc, done func(keepCtx bool), chOut chanOut) { + // Not sure if we need to sanitize the incoming req.Method or not. + ctx, span := s.getSpan(ctx, req) + ctx, _ = tag.New(ctx, tag.Insert(metrics.RPCMethod, req.Method)) + defer span.End() + + handler, ok := s.methods[req.Method] + if !ok { + aliasTo, ok := s.aliasedMethods[req.Method] + if ok { + handler, ok = s.methods[aliasTo] + } + if !ok { + rpcError(w, &req, rpcMethodNotFound, fmt.Errorf("method '%s' not found", req.Method)) + stats.Record(ctx, metrics.RPCInvalidMethod.M(1)) + done(false) + return + } + } + + outCh := handler.valOut != -1 && handler.handlerFunc.Type().Out(handler.valOut).Kind() == reflect.Chan + defer done(outCh) + + if chOut == nil && outCh { + rpcError(w, &req, rpcMethodNotFound, fmt.Errorf("method '%s' not supported in this mode (no out channel support)", req.Method)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + return + } + + callParams := make([]reflect.Value, 1+handler.hasCtx+handler.nParams) + callParams[0] = handler.receiver + if handler.hasCtx == 1 { + callParams[1] = reflect.ValueOf(ctx) + } + + if handler.hasRawParams { + // When hasRawParams is true, there is only one parameter and it is a + // json.RawMessage. + + callParams[1+handler.hasCtx] = reflect.ValueOf(RawParams(req.Params)) + } else { + // "normal" param list; no good way to do named params in Golang + + var ps []param + if len(req.Params) > 0 { + err := json.Unmarshal(req.Params, &ps) + if err != nil { + rpcError(w, &req, rpcParseError, xerrors.Errorf("unmarshaling param array: %w", err)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + return + } + } + + if len(ps) != handler.nParams { + rpcError(w, &req, rpcInvalidParams, fmt.Errorf("wrong param count (method '%s'): %d != %d", req.Method, len(ps), handler.nParams)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + done(false) + return + } + + for i := 0; i < handler.nParams; i++ { + var rp reflect.Value + + typ := handler.paramReceivers[i] + dec, found := s.paramDecoders[typ] + if !found { + rp = reflect.New(typ) + if err := json.NewDecoder(bytes.NewReader(ps[i].data)).Decode(rp.Interface()); err != nil { + rpcError(w, &req, rpcParseError, xerrors.Errorf("unmarshaling params for '%s' (param: %T): %w", req.Method, rp.Interface(), err)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + return + } + rp = rp.Elem() + } else { + var err error + rp, err = dec(ctx, ps[i].data) + if err != nil { + rpcError(w, &req, rpcParseError, xerrors.Errorf("decoding params for '%s' (param: %d; custom decoder): %w", req.Method, i, err)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + return + } + } + + callParams[i+1+handler.hasCtx] = reflect.ValueOf(rp.Interface()) + } + } + + // ///////////////// + + callResult, err := doCall(req.Method, handler.handlerFunc, callParams) + if err != nil { + rpcError(w, &req, 0, xerrors.Errorf("fatal error calling '%s': %w", req.Method, err)) + stats.Record(ctx, metrics.RPCRequestError.M(1)) + if s.tracer != nil { + s.tracer(req.Method, callParams, nil, err) + } + return + } + if req.ID == nil { + return // notification + } + + if s.tracer != nil { + s.tracer(req.Method, callParams, callResult, nil) + } + // ///////////////// + + resp := response{ + Jsonrpc: "2.0", + ID: req.ID, + } + + if handler.errOut != -1 { + err := callResult[handler.errOut].Interface() + if err != nil { + log.Warnf("error in RPC call to '%s': %+v", req.Method, err) + stats.Record(ctx, metrics.RPCResponseError.M(1)) + + resp.Error = s.createError(err.(error)) + } + } + + var kind reflect.Kind + var res interface{} + var nonZero bool + if handler.valOut != -1 { + res = callResult[handler.valOut].Interface() + kind = callResult[handler.valOut].Kind() + nonZero = !callResult[handler.valOut].IsZero() + } + + // check error as JSON-RPC spec prohibits error and value at the same time + if resp.Error == nil { + if res != nil && kind == reflect.Chan { + // Channel responses are sent from channel control goroutine. + // Sending responses here could cause deadlocks on writeLk, or allow + // sending channel messages before this rpc call returns + + //noinspection GoNilness // already checked above + err = chOut(callResult[handler.valOut], req.ID) + if err == nil { + return // channel goroutine handles responding + } + + log.Warnf("failed to setup channel in RPC call to '%s': %+v", req.Method, err) + stats.Record(ctx, metrics.RPCResponseError.M(1)) + + resp.Error = &JSONRPCError{ + Code: 1, + Message: err.Error(), + } + } else { + resp.Result = res + } + } + if resp.Error != nil && nonZero { + log.Errorw("error and res returned", "request", req, "r.err", resp.Error, "res", res) + } + + withLazyWriter(w, func(w io.Writer) { + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Error(err) + stats.Record(ctx, metrics.RPCResponseError.M(1)) + return + } + }) +} + +// withLazyWriter makes it possible to defer acquiring a writer until the first write. +// This is useful because json.Encode needs to marshal the response fully before writing, which may be +// a problem for very large responses. +func withLazyWriter(withWriterFunc func(func(io.Writer)), cb func(io.Writer)) { + lw := &lazyWriter{ + withWriterFunc: withWriterFunc, + + done: make(chan struct{}), + } + + defer close(lw.done) + cb(lw) +} + +type lazyWriter struct { + withWriterFunc func(func(io.Writer)) + + w io.Writer + done chan struct{} +} + +func (lw *lazyWriter) Write(p []byte) (n int, err error) { + if lw.w == nil { + acquired := make(chan struct{}) + go func() { + lw.withWriterFunc(func(w io.Writer) { + lw.w = w + close(acquired) + <-lw.done + }) + }() + <-acquired + } + + return lw.w.Write(p) +} diff --git a/common/serialize/go-jsonrpc/httpio/README b/common/serialize/go-jsonrpc/httpio/README new file mode 100644 index 000000000..a44f5c6d8 --- /dev/null +++ b/common/serialize/go-jsonrpc/httpio/README @@ -0,0 +1,2 @@ +This package provides param encoders / decoders for `io.Reader` which proxy +data over temporary http endpoints \ No newline at end of file diff --git a/common/serialize/go-jsonrpc/httpio/reader.go b/common/serialize/go-jsonrpc/httpio/reader.go new file mode 100644 index 000000000..4f378dbf7 --- /dev/null +++ b/common/serialize/go-jsonrpc/httpio/reader.go @@ -0,0 +1,142 @@ +package httpio + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "reflect" + "sync" + + "github.com/google/uuid" + logging "github.com/ipfs/go-log/v2" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-jsonrpc" +) + +var log = logging.Logger("rpc") + +func ReaderParamEncoder(addr string) jsonrpc.Option { + return jsonrpc.WithParamEncoder(new(io.Reader), func(value reflect.Value) (reflect.Value, error) { + r := value.Interface().(io.Reader) + + reqID := uuid.New() + u, _ := url.Parse(addr) + u.Path = path.Join(u.Path, reqID.String()) + + go func() { + // TODO: figure out errors here + + resp, err := http.Post(u.String(), "application/octet-stream", r) + if err != nil { + log.Errorf("sending reader param: %+v", err) + return + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Errorf("sending reader param: non-200 status: ", resp.Status) + return + } + + }() + + return reflect.ValueOf(reqID), nil + }) +} + +type waitReadCloser struct { + io.ReadCloser + wait chan struct{} +} + +func (w *waitReadCloser) Read(p []byte) (int, error) { + n, err := w.ReadCloser.Read(p) + if err != nil { + close(w.wait) + } + return n, err +} + +func (w *waitReadCloser) Close() error { + close(w.wait) + return w.ReadCloser.Close() +} + +func ReaderParamDecoder() (http.HandlerFunc, jsonrpc.ServerOption) { + var readersLk sync.Mutex + readers := map[uuid.UUID]chan *waitReadCloser{} + + hnd := func(resp http.ResponseWriter, req *http.Request) { + strId := path.Base(req.URL.Path) + u, err := uuid.Parse(strId) + if err != nil { + http.Error(resp, fmt.Sprintf("parsing reader uuid: %s", err), 400) + } + + readersLk.Lock() + ch, found := readers[u] + if !found { + ch = make(chan *waitReadCloser) + readers[u] = ch + } + readersLk.Unlock() + + wr := &waitReadCloser{ + ReadCloser: req.Body, + wait: make(chan struct{}), + } + + select { + case ch <- wr: + case <-req.Context().Done(): + log.Error("context error in reader stream handler (1): %v", req.Context().Err()) + resp.WriteHeader(500) + return + } + + select { + case <-wr.wait: + case <-req.Context().Done(): + log.Error("context error in reader stream handler (2): %v", req.Context().Err()) + resp.WriteHeader(500) + return + } + + resp.WriteHeader(200) + } + + dec := jsonrpc.WithParamDecoder(new(io.Reader), func(ctx context.Context, b []byte) (reflect.Value, error) { + var strId string + if err := json.Unmarshal(b, &strId); err != nil { + return reflect.Value{}, xerrors.Errorf("unmarshaling reader id: %w", err) + } + + u, err := uuid.Parse(strId) + if err != nil { + return reflect.Value{}, xerrors.Errorf("parsing reader UUDD: %w", err) + } + + readersLk.Lock() + ch, found := readers[u] + if !found { + ch = make(chan *waitReadCloser) + readers[u] = ch + } + readersLk.Unlock() + + select { + case wr := <-ch: + return reflect.ValueOf(wr), nil + case <-ctx.Done(): + return reflect.Value{}, ctx.Err() + } + }) + + return hnd, dec +} diff --git a/common/serialize/go-jsonrpc/httpio/reader_test.go b/common/serialize/go-jsonrpc/httpio/reader_test.go new file mode 100644 index 000000000..9aa2d8d35 --- /dev/null +++ b/common/serialize/go-jsonrpc/httpio/reader_test.go @@ -0,0 +1,54 @@ +package httpio + +import ( + "context" + "io" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-jsonrpc" +) + +type ReaderHandler struct { +} + +func (h *ReaderHandler) ReadAll(ctx context.Context, r io.Reader) ([]byte, error) { + return io.ReadAll(r) +} + +func (h *ReaderHandler) ReadUrl(ctx context.Context, u string) (string, error) { + return u, nil +} + +func TestReaderProxy(t *testing.T) { + var client struct { + ReadAll func(ctx context.Context, r io.Reader) ([]byte, error) + } + + serverHandler := &ReaderHandler{} + + readerHandler, readerServerOpt := ReaderParamDecoder() + rpcServer := jsonrpc.NewServer(readerServerOpt) + rpcServer.Register("ReaderHandler", serverHandler) + + mux := mux.NewRouter() + mux.Handle("/rpc/v0", rpcServer) + mux.Handle("/rpc/streams/v0/push/{uuid}", readerHandler) + + testServ := httptest.NewServer(mux) + defer testServ.Close() + + re := ReaderParamEncoder("http://" + testServ.Listener.Addr().String() + "/rpc/streams/v0/push") + closer, err := jsonrpc.NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String()+"/rpc/v0", "ReaderHandler", []interface{}{&client}, nil, re) + require.NoError(t, err) + + defer closer() + + read, err := client.ReadAll(context.TODO(), strings.NewReader("pooooootato")) + require.NoError(t, err) + require.Equal(t, "pooooootato", string(read), "potatos weren't equal") +} diff --git a/common/serialize/go-jsonrpc/method_formatter.go b/common/serialize/go-jsonrpc/method_formatter.go new file mode 100644 index 000000000..1248a52fe --- /dev/null +++ b/common/serialize/go-jsonrpc/method_formatter.go @@ -0,0 +1,38 @@ +package jsonrpc + +import "strings" + +// MethodNameFormatter is a function that takes a namespace and a method name and returns the full method name, sent via JSON-RPC. +// This is useful if you want to customize the default behaviour, e.g. send without the namespace or make it lowercase. +type MethodNameFormatter func(namespace, method string) string + +// CaseStyle represents the case style for method names. +type CaseStyle int + +const ( + OriginalCase CaseStyle = iota + LowerFirstCharCase + AllFirstCharCase +) + +// NewMethodNameFormatter creates a new method name formatter based on the provided options. +func NewMethodNameFormatter(includeNamespace bool, nameCase CaseStyle) MethodNameFormatter { + return func(namespace, method string) string { + formattedMethod := method + if nameCase == LowerFirstCharCase && len(method) > 0 { + formattedMethod = strings.ToLower(method[:1]) + method[1:] + } + if nameCase == AllFirstCharCase { + return strings.ToLower(namespace + "." + formattedMethod) + + } + + if includeNamespace { + return namespace + "." + formattedMethod + } + return formattedMethod + } +} + +// DefaultMethodNameFormatter is a pass-through formatter with default options. +var DefaultMethodNameFormatter = NewMethodNameFormatter(false, OriginalCase) diff --git a/common/serialize/go-jsonrpc/method_formatter_test.go b/common/serialize/go-jsonrpc/method_formatter_test.go new file mode 100644 index 000000000..d003ddd54 --- /dev/null +++ b/common/serialize/go-jsonrpc/method_formatter_test.go @@ -0,0 +1,125 @@ +package jsonrpc + +import ( + "context" + "fmt" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestDifferentMethodNamers(t *testing.T) { + tests := map[string]struct { + namer MethodNameFormatter + + requestedMethod string + }{ + "default namer": { + namer: DefaultMethodNameFormatter, + requestedMethod: "SimpleServerHandler.Inc", + }, + "lower fist char": { + namer: NewMethodNameFormatter(true, LowerFirstCharCase), + requestedMethod: "SimpleServerHandler.inc", + }, + "no namespace namer": { + namer: NewMethodNameFormatter(false, OriginalCase), + requestedMethod: "Inc", + }, + "no namespace & lower fist char": { + namer: NewMethodNameFormatter(false, LowerFirstCharCase), + requestedMethod: "inc", + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rpcServer := NewServer(WithServerMethodNameFormatter(test.namer)) + + serverHandler := &SimpleServerHandler{} + rpcServer.Register("SimpleServerHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + req := fmt.Sprintf(`{"jsonrpc": "2.0", "method": "%s", "params": [], "id": 1}`, test.requestedMethod) + + res, err := http.Post(testServ.URL, "application/json", strings.NewReader(req)) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, int32(1), serverHandler.n) + }) + } +} + +func TestDifferentMethodNamersWithClient(t *testing.T) { + tests := map[string]struct { + namer MethodNameFormatter + urlPrefix string + }{ + "default namer & http": { + namer: DefaultMethodNameFormatter, + urlPrefix: "http://", + }, + "default namer & ws": { + namer: DefaultMethodNameFormatter, + urlPrefix: "ws://", + }, + "lower first char namer & http": { + namer: NewMethodNameFormatter(true, LowerFirstCharCase), + urlPrefix: "http://", + }, + "lower first char namer & ws": { + namer: NewMethodNameFormatter(true, LowerFirstCharCase), + urlPrefix: "ws://", + }, + "no namespace namer & http": { + namer: NewMethodNameFormatter(false, OriginalCase), + urlPrefix: "http://", + }, + "no namespace namer & ws": { + namer: NewMethodNameFormatter(false, OriginalCase), + urlPrefix: "ws://", + }, + "no namespace & lower first char & http": { + namer: NewMethodNameFormatter(false, LowerFirstCharCase), + urlPrefix: "http://", + }, + "no namespace & lower first char & ws": { + namer: NewMethodNameFormatter(false, LowerFirstCharCase), + urlPrefix: "ws://", + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rpcServer := NewServer(WithServerMethodNameFormatter(test.namer)) + + serverHandler := &SimpleServerHandler{} + rpcServer.Register("SimpleServerHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + var client struct { + AddGet func(int) int + } + + closer, err := NewMergeClient( + context.Background(), + test.urlPrefix+testServ.Listener.Addr().String(), + "SimpleServerHandler", + []any{&client}, + nil, + WithHTTPClient(testServ.Client()), + WithMethodNameFormatter(test.namer), + ) + require.NoError(t, err) + defer closer() + + n := client.AddGet(123) + require.Equal(t, 123, n) + }) + } +} diff --git a/common/serialize/go-jsonrpc/metrics/metrics.go b/common/serialize/go-jsonrpc/metrics/metrics.go new file mode 100644 index 000000000..550f93671 --- /dev/null +++ b/common/serialize/go-jsonrpc/metrics/metrics.go @@ -0,0 +1,45 @@ +package metrics + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +// Global Tags +var ( + RPCMethod, _ = tag.NewKey("method") +) + +// Measures +var ( + RPCInvalidMethod = stats.Int64("rpc/invalid_method", "Total number of invalid RPC methods called", stats.UnitDimensionless) + RPCRequestError = stats.Int64("rpc/request_error", "Total number of request errors handled", stats.UnitDimensionless) + RPCResponseError = stats.Int64("rpc/response_error", "Total number of responses errors handled", stats.UnitDimensionless) +) + +var ( + // All RPC related metrics should at the very least tag the RPCMethod + RPCInvalidMethodView = &view.View{ + Measure: RPCInvalidMethod, + Aggregation: view.Count(), + TagKeys: []tag.Key{RPCMethod}, + } + RPCRequestErrorView = &view.View{ + Measure: RPCRequestError, + Aggregation: view.Count(), + TagKeys: []tag.Key{RPCMethod}, + } + RPCResponseErrorView = &view.View{ + Measure: RPCResponseError, + Aggregation: view.Count(), + TagKeys: []tag.Key{RPCMethod}, + } +) + +// DefaultViews is an array of OpenCensus views for metric gathering purposes +var DefaultViews = []*view.View{ + RPCInvalidMethodView, + RPCRequestErrorView, + RPCResponseErrorView, +} diff --git a/common/serialize/go-jsonrpc/options.go b/common/serialize/go-jsonrpc/options.go new file mode 100644 index 000000000..8b4d0e971 --- /dev/null +++ b/common/serialize/go-jsonrpc/options.go @@ -0,0 +1,122 @@ +package jsonrpc + +import ( + "net/http" + "reflect" + "time" + + "github.com/gorilla/websocket" +) + +type ParamEncoder func(reflect.Value) (reflect.Value, error) + +type clientHandler struct { + ns string + hnd interface{} +} + +type Config struct { + reconnectBackoff backoff + pingInterval time.Duration + timeout time.Duration + + paramEncoders map[reflect.Type]ParamEncoder + errors *Errors + + reverseHandlers []clientHandler + aliasedHandlerMethods map[string]string + + httpClient *http.Client + + noReconnect bool + proxyConnFactory func(func() (*websocket.Conn, error)) func() (*websocket.Conn, error) // for testing + + methodNamer MethodNameFormatter +} + +func defaultConfig() Config { + return Config{ + reconnectBackoff: backoff{ + minDelay: 100 * time.Millisecond, + maxDelay: 5 * time.Second, + }, + pingInterval: 5 * time.Second, + timeout: 30 * time.Second, + + aliasedHandlerMethods: map[string]string{}, + + paramEncoders: map[reflect.Type]ParamEncoder{}, + + httpClient: _defaultHTTPClient, + + methodNamer: DefaultMethodNameFormatter, + } +} + +type Option func(c *Config) + +func WithReconnectBackoff(minDelay, maxDelay time.Duration) func(c *Config) { + return func(c *Config) { + c.reconnectBackoff = backoff{ + minDelay: minDelay, + maxDelay: maxDelay, + } + } +} + +// Must be < Timeout/2 +func WithPingInterval(d time.Duration) func(c *Config) { + return func(c *Config) { + c.pingInterval = d + } +} + +func WithTimeout(d time.Duration) func(c *Config) { + return func(c *Config) { + c.timeout = d + } +} + +func WithNoReconnect() func(c *Config) { + return func(c *Config) { + c.noReconnect = true + } +} + +func WithParamEncoder(t interface{}, encoder ParamEncoder) func(c *Config) { + return func(c *Config) { + c.paramEncoders[reflect.TypeOf(t).Elem()] = encoder + } +} + +func WithErrors(es Errors) func(c *Config) { + return func(c *Config) { + c.errors = &es + } +} + +func WithClientHandler(ns string, hnd interface{}) func(c *Config) { + return func(c *Config) { + c.reverseHandlers = append(c.reverseHandlers, clientHandler{ns, hnd}) + } +} + +// WithClientHandlerAlias creates an alias for a client HANDLER method - for handlers created +// with WithClientHandler +func WithClientHandlerAlias(alias, original string) func(c *Config) { + return func(c *Config) { + c.aliasedHandlerMethods[alias] = original + } +} + +func WithHTTPClient(h *http.Client) func(c *Config) { + return func(c *Config) { + c.httpClient = h + } +} + +func WithMethodNameFormatter(namer MethodNameFormatter) func(c *Config) { + return func(c *Config) { + c.methodNamer = namer + } +} diff --git a/common/serialize/go-jsonrpc/options_server.go b/common/serialize/go-jsonrpc/options_server.go new file mode 100644 index 000000000..c6897a43d --- /dev/null +++ b/common/serialize/go-jsonrpc/options_server.go @@ -0,0 +1,125 @@ +package jsonrpc + +import ( + "context" + "reflect" + "time" + + "golang.org/x/xerrors" +) + +// note: we embed reflect.Type because proxy-structs are not comparable +type jsonrpcReverseClient struct{ reflect.Type } + +type ParamDecoder func(ctx context.Context, json []byte) (reflect.Value, error) + +type ServerConfig struct { + maxRequestSize int64 + pingInterval time.Duration + + paramDecoders map[reflect.Type]ParamDecoder + errors *Errors + + reverseClientBuilder func(context.Context, *wsConn) (context.Context, error) + tracer Tracer + methodNameFormatter MethodNameFormatter +} + +type ServerOption func(c *ServerConfig) + +func defaultServerConfig() ServerConfig { + return ServerConfig{ + paramDecoders: map[reflect.Type]ParamDecoder{}, + maxRequestSize: DEFAULT_MAX_REQUEST_SIZE, + + pingInterval: 5 * time.Second, + methodNameFormatter: DefaultMethodNameFormatter, + } +} + +func WithParamDecoder(t interface{}, decoder ParamDecoder) ServerOption { + return func(c *ServerConfig) { + c.paramDecoders[reflect.TypeOf(t).Elem()] = decoder + } +} + +func WithMaxRequestSize(max int64) ServerOption { + return func(c *ServerConfig) { + c.maxRequestSize = max + } +} + +func WithServerErrors(es Errors) ServerOption { + return func(c *ServerConfig) { + c.errors = &es + } +} + +func WithServerPingInterval(d time.Duration) ServerOption { + return func(c *ServerConfig) { + c.pingInterval = d + } +} + +func WithServerMethodNameFormatter(formatter MethodNameFormatter) ServerOption { + return func(c *ServerConfig) { + c.methodNameFormatter = formatter + } +} + +// WithTracer allows the instantiator to trace the method calls and results. +// This is useful for debugging a client-server interaction. +func WithTracer(l Tracer) ServerOption { + return func(c *ServerConfig) { + c.tracer = l + } +} + +// WithReverseClient will allow extracting reverse client on **WEBSOCKET** calls. +// RP is a proxy-struct type, much like the one passed to NewClient. +func WithReverseClient[RP any](namespace string) ServerOption { + return func(c *ServerConfig) { + c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) { + cl := client{ + namespace: namespace, + paramEncoders: map[reflect.Type]ParamEncoder{}, + methodNameFormatter: c.methodNameFormatter, + } + + // todo test that everything is closing correctly + cl.exiting = conn.exiting + + requests := cl.setupRequestChan() + conn.requests = requests + + calls := new(RP) + + err := cl.provide([]interface{}{ + calls, + }) + if err != nil { + return nil, xerrors.Errorf("provide reverse client calls: %w", err) + } + + return context.WithValue(ctx, jsonrpcReverseClient{reflect.TypeOf(calls).Elem()}, calls), nil + } + } +} + +// ExtractReverseClient will extract reverse client from context. Reverse client for the type +// will only be present if the server was constructed with a matching WithReverseClient option +// and the connection was a websocket connection. +// If there is no reverse client, the call will return a zero value and `false`. Otherwise a reverse +// client and `true` will be returned. +func ExtractReverseClient[C any](ctx context.Context) (C, bool) { + c, ok := ctx.Value(jsonrpcReverseClient{reflect.TypeOf(new(C)).Elem()}).(*C) + if !ok { + return *new(C), false + } + if c == nil { + // something is very wrong, but don't panic + return *new(C), false + } + + return *c, ok +} diff --git a/common/serialize/go-jsonrpc/resp_error_test.go b/common/serialize/go-jsonrpc/resp_error_test.go new file mode 100644 index 000000000..e5b2bec92 --- /dev/null +++ b/common/serialize/go-jsonrpc/resp_error_test.go @@ -0,0 +1,306 @@ +package jsonrpc + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type ComplexData struct { + Foo string `json:"foo"` + Bar int `json:"bar"` +} + +type StaticError struct{} + +func (e *StaticError) Error() string { return "static error" } + +// Define the error types +type SimpleError struct { + Message string +} + +func (e *SimpleError) Error() string { + return e.Message +} + +func (e *SimpleError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + return nil +} + +func (e *SimpleError) ToJSONRPCError() (JSONRPCError, error) { + return JSONRPCError{Message: e.Message}, nil +} + +var _ RPCErrorCodec = (*SimpleError)(nil) + +type DataStringError struct { + Message string `json:"message"` + Data string `json:"data"` +} + +func (e *DataStringError) Error() string { + return e.Message +} + +func (e *DataStringError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + data, ok := jerr.Data.(string) + if !ok { + return fmt.Errorf("expected string data, got %T", jerr.Data) + } + + e.Data = data + + return nil +} + +func (e *DataStringError) ToJSONRPCError() (JSONRPCError, error) { + return JSONRPCError{Message: e.Message, Data: e.Data}, nil +} + +var _ RPCErrorCodec = (*DataStringError)(nil) + +type DataComplexError struct { + Message string + internalData ComplexData +} + +func (e *DataComplexError) Error() string { + return e.Message +} + +func (e *DataComplexError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + data, ok := jerr.Data.(json.RawMessage) + if !ok { + return fmt.Errorf("expected string data, got %T", jerr.Data) + } + + if err := json.Unmarshal(data, &e.internalData); err != nil { + return err + } + return nil +} + +func (e *DataComplexError) ToJSONRPCError() (JSONRPCError, error) { + data, err := json.Marshal(e.internalData) + if err != nil { + return JSONRPCError{}, err + } + return JSONRPCError{Message: e.Message, Data: data}, nil +} + +var _ RPCErrorCodec = (*DataComplexError)(nil) + +type MetaError struct { + Message string + Details string +} + +func (e *MetaError) Error() string { + return e.Message +} + +func (e *MetaError) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Message string `json:"message"` + Details string `json:"details"` + }{ + Message: e.Message, + Details: e.Details, + }) +} + +func (e *MetaError) UnmarshalJSON(data []byte) error { + var temp struct { + Message string `json:"message"` + Details string `json:"details"` + } + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + e.Message = temp.Message + e.Details = temp.Details + return nil +} + +type ComplexError struct { + Message string + Data ComplexData + Details string +} + +func (e *ComplexError) Error() string { + return e.Message +} + +func (e *ComplexError) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Message string `json:"message"` + Details string `json:"details"` + Data any `json:"data"` + }{ + Details: e.Details, + Message: e.Message, + Data: e.Data, + }) +} + +func (e *ComplexError) UnmarshalJSON(data []byte) error { + var temp struct { + Message string `json:"message"` + Details string `json:"details"` + Data ComplexData `json:"data"` + } + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + e.Details = temp.Details + e.Message = temp.Message + e.Data = temp.Data + return nil +} + +func TestRespErrorVal(t *testing.T) { + // Initialize the Errors struct and register error types + errorsMap := NewErrors() + errorsMap.Register(1000, new(*StaticError)) + errorsMap.Register(1001, new(*SimpleError)) + errorsMap.Register(1002, new(*DataStringError)) + errorsMap.Register(1003, new(*DataComplexError)) + errorsMap.Register(1004, new(*MetaError)) + errorsMap.Register(1005, new(*ComplexError)) + + // Define test cases + testCases := []struct { + name string + respError *JSONRPCError + expectedType interface{} + expectedMessage string + verify func(t *testing.T, err error) + }{ + { + name: "StaticError", + respError: &JSONRPCError{ + Code: 1000, + Message: "this is ignored", + }, + expectedType: &StaticError{}, + expectedMessage: "static error", + }, + { + name: "SimpleError", + respError: &JSONRPCError{ + Code: 1001, + Message: "simple error occurred", + }, + expectedType: &SimpleError{}, + expectedMessage: "simple error occurred", + }, + { + name: "DataStringError", + respError: &JSONRPCError{ + Code: 1002, + Message: "data error occurred", + Data: "additional data", + }, + expectedType: &DataStringError{}, + expectedMessage: "data error occurred", + verify: func(t *testing.T, err error) { + require.IsType(t, &DataStringError{}, err) + require.Equal(t, "data error occurred", err.Error()) + require.Equal(t, "additional data", err.(*DataStringError).Data) + }, + }, + { + name: "DataComplexError", + respError: &JSONRPCError{ + Code: 1003, + Message: "data error occurred", + Data: json.RawMessage(`{"foo":"boop","bar":101}`), + }, + expectedType: &DataComplexError{}, + expectedMessage: "data error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, ComplexData{Foo: "boop", Bar: 101}, err.(*DataComplexError).internalData) + }, + }, + { + name: "MetaError", + respError: &JSONRPCError{ + Code: 1004, + Message: "meta error occurred", + Meta: func() json.RawMessage { + me := &MetaError{ + Message: "meta error occurred", + Details: "meta details", + } + metaData, _ := me.MarshalJSON() + return metaData + }(), + }, + expectedType: &MetaError{}, + expectedMessage: "meta error occurred", + verify: func(t *testing.T, err error) { + // details will also be included in the error message since it implements the marshable interface + require.Equal(t, "meta details", err.(*MetaError).Details) + }, + }, + { + name: "ComplexError", + respError: &JSONRPCError{ + Code: 1005, + Message: "complex error occurred", + Data: json.RawMessage(`"complex data"`), + Meta: func() json.RawMessage { + ce := &ComplexError{ + Message: "complex error occurred", + Details: "complex details", + Data: ComplexData{Foo: "foo", Bar: 42}, + } + metaData, _ := ce.MarshalJSON() + return metaData + }(), + }, + expectedType: &ComplexError{}, + expectedMessage: "complex error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, ComplexData{Foo: "foo", Bar: 42}, err.(*ComplexError).Data) + require.Equal(t, "complex details", err.(*ComplexError).Details) + }, + }, + { + name: "UnregisteredError", + respError: &JSONRPCError{ + Code: 9999, + Message: "unregistered error occurred", + Data: json.RawMessage(`"some data"`), + }, + expectedType: &JSONRPCError{}, + expectedMessage: "unregistered error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, json.RawMessage(`"some data"`), err.(*JSONRPCError).Data) + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + errValue := tc.respError.val(&errorsMap) + errInterface := errValue.Interface() + err, ok := errInterface.(error) + require.True(t, ok, "returned value does not implement error interface") + require.IsType(t, tc.expectedType, err) + require.Equal(t, tc.expectedMessage, err.Error()) + if tc.verify != nil { + tc.verify(t, err) + } + }) + } +} diff --git a/common/serialize/go-jsonrpc/response.go b/common/serialize/go-jsonrpc/response.go new file mode 100644 index 000000000..7bf866d94 --- /dev/null +++ b/common/serialize/go-jsonrpc/response.go @@ -0,0 +1,89 @@ +package jsonrpc + +import ( + "encoding/json" + "fmt" + "reflect" +) + +type response struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + ID interface{} `json:"id"` + Error *JSONRPCError `json:"error,omitempty"` +} + +func (r response) MarshalJSON() ([]byte, error) { + // Custom marshal logic as per JSON-RPC 2.0 spec: + // > `result`: + // > This member is REQUIRED on success. + // > This member MUST NOT exist if there was an error invoking the method. + // + // > `error`: + // > This member is REQUIRED on error. + // > This member MUST NOT exist if there was no error triggered during invocation. + data := map[string]interface{}{ + "jsonrpc": r.Jsonrpc, + "id": r.ID, + } + + if r.Error != nil { + data["error"] = r.Error + } else { + data["result"] = r.Result + } + return json.Marshal(data) +} + +type JSONRPCError struct { + Code ErrorCode `json:"code"` + Message string `json:"message"` + Meta json.RawMessage `json:"meta,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +func (e *JSONRPCError) Error() string { + if e.Code >= -32768 && e.Code <= -32000 { + return fmt.Sprintf("RPC error (%d): %s", e.Code, e.Message) + } + return e.Message +} + +var ( + _ error = (*JSONRPCError)(nil) + marshalableRT = reflect.TypeOf(new(marshalable)).Elem() + errorCodecRT = reflect.TypeOf(new(RPCErrorCodec)).Elem() +) + +func (e *JSONRPCError) val(errors *Errors) reflect.Value { + if errors != nil { + t, ok := errors.byCode[e.Code] + if ok { + var v reflect.Value + if t.Kind() == reflect.Ptr { + v = reflect.New(t.Elem()) + } else { + v = reflect.New(t) + } + + if v.Type().Implements(errorCodecRT) { + if err := v.Interface().(RPCErrorCodec).FromJSONRPCError(*e); err != nil { + log.Errorf("Error converting JSONRPCError to custom error type '%s' (code %d): %w", t.String(), e.Code, err) + return reflect.ValueOf(e) + } + } else if len(e.Meta) > 0 && v.Type().Implements(marshalableRT) { + if err := v.Interface().(marshalable).UnmarshalJSON(e.Meta); err != nil { + log.Errorf("Error unmarshalling error metadata to custom error type '%s' (code %d): %w", t.String(), e.Code, err) + return reflect.ValueOf(e) + } + } + + if t.Kind() != reflect.Ptr { + v = v.Elem() + } + return v + } + } + + return reflect.ValueOf(e) +} diff --git a/common/serialize/go-jsonrpc/rpc_test.go b/common/serialize/go-jsonrpc/rpc_test.go new file mode 100644 index 000000000..f3947089d --- /dev/null +++ b/common/serialize/go-jsonrpc/rpc_test.go @@ -0,0 +1,1751 @@ +package jsonrpc + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "reflect" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/gorilla/websocket" + logging "github.com/ipfs/go-log/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" +) + +func init() { + if _, exists := os.LookupEnv("GOLOG_LOG_LEVEL"); !exists { + if err := logging.SetLogLevel("rpc", "DEBUG"); err != nil { + panic(err) + } + } + + debugTrace = true +} + +type SimpleServerHandler struct { + n int32 +} + +type TestType struct { + S string + I int +} + +type TestOut struct { + TestType + Ok bool +} + +func (h *SimpleServerHandler) Inc() error { + h.n++ + + return nil +} + +func (h *SimpleServerHandler) Add(in int) error { + if in == -3546 { + return errors.New("test") + } + + atomic.AddInt32(&h.n, int32(in)) + + return nil +} + +func (h *SimpleServerHandler) AddGet(in int) int { + atomic.AddInt32(&h.n, int32(in)) + return int(h.n) +} + +func (h *SimpleServerHandler) StringMatch(t TestType, i2 int64) (out TestOut, err error) { + if strconv.FormatInt(i2, 10) == t.S { + out.Ok = true + } + if i2 != int64(t.I) { + return TestOut{}, errors.New(":(") + } + out.I = t.I + out.S = t.S + return +} + +func TestRawRequests(t *testing.T) { + rpcHandler := SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", &rpcHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + removeSpaces := func(jsonStr string) (string, error) { + var jsonObj interface{} + err := json.Unmarshal([]byte(jsonStr), &jsonObj) + if err != nil { + return "", err + } + + compactJSONBytes, err := json.Marshal(jsonObj) + if err != nil { + return "", err + } + + return string(compactJSONBytes), nil + } + + tc := func(req, resp string, n int32, statusCode int) func(t *testing.T) { + return func(t *testing.T) { + rpcHandler.n = 0 + + res, err := http.Post(testServ.URL, "application/json", strings.NewReader(req)) + require.NoError(t, err) + + b, err := io.ReadAll(res.Body) + require.NoError(t, err) + + expectedResp, err := removeSpaces(resp) + require.NoError(t, err) + + responseBody, err := removeSpaces(string(b)) + require.NoError(t, err) + + assert.Equal(t, expectedResp, responseBody) + require.Equal(t, n, rpcHandler.n) + require.Equal(t, statusCode, res.StatusCode) + } + } + + t.Run("inc", tc(`{"jsonrpc": "2.0", "method": "SimpleServerHandler.Inc", "params": [], "id": 1}`, `{"jsonrpc":"2.0","id":1,"result":null}`, 1, 200)) + t.Run("inc-null", tc(`{"jsonrpc": "2.0", "method": "SimpleServerHandler.Inc", "params": null, "id": 1}`, `{"jsonrpc":"2.0","id":1,"result":null}`, 1, 200)) + t.Run("inc-noparam", tc(`{"jsonrpc": "2.0", "method": "SimpleServerHandler.Inc", "id": 2}`, `{"jsonrpc":"2.0","id":2,"result":null}`, 1, 200)) + t.Run("add", tc(`{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [10], "id": 4}`, `{"jsonrpc":"2.0","id":4,"result":null}`, 10, 200)) + // Batch requests + t.Run("add", tc(`[{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [123], "id": 5}`, `{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}`, 0, 500)) + t.Run("add", tc(`[{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [123], "id": 6}]`, `[{"jsonrpc":"2.0","id":6,"result":null}]`, 123, 200)) + t.Run("add", tc(`[{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [123], "id": 7},{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [-122], "id": 8}]`, `[{"jsonrpc":"2.0","id":7,"result":null},{"jsonrpc":"2.0","id":8,"result":null}]`, 1, 200)) + t.Run("add", tc(`[{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [123], "id": 9},{"jsonrpc": "2.0", "params": [-122], "id": 10}]`, `[{"jsonrpc":"2.0","id":9,"result":null},{"error":{"code":-32601,"message":"method '' not found"},"id":10,"jsonrpc":"2.0"}]`, 123, 200)) + t.Run("add", tc(` [{"jsonrpc": "2.0", "method": "SimpleServerHandler.Add", "params": [-1], "id": 11}] `, `[{"jsonrpc":"2.0","id":11,"result":null}]`, -1, 200)) + t.Run("add", tc(``, `{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Invalid request"}}`, 0, 400)) +} + +func TestReconnection(t *testing.T) { + var rpcClient struct { + Add func(int) error + } + + rpcHandler := SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", &rpcHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // capture connection attempts for this duration + captureDuration := 3 * time.Second + + // run the test until the timer expires + timer := time.NewTimer(captureDuration) + + // record the number of connection attempts during this test + connectionAttempts := int64(1) + + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", []interface{}{&rpcClient}, nil, func(c *Config) { + c.proxyConnFactory = func(f func() (*websocket.Conn, error)) func() (*websocket.Conn, error) { + return func() (*websocket.Conn, error) { + defer func() { + atomic.AddInt64(&connectionAttempts, 1) + }() + + if atomic.LoadInt64(&connectionAttempts) > 1 { + return nil, errors.New("simulates a failed reconnect attempt") + } + + c, err := f() + if err != nil { + return nil, err + } + + // closing the connection here triggers the reconnect logic + _ = c.Close() + + return c, nil + } + } + }) + require.NoError(t, err) + defer closer() + + // let the JSON-RPC library attempt to reconnect until the timer runs out + <-timer.C + + // do some math + attemptsPerSecond := atomic.LoadInt64(&connectionAttempts) / int64(captureDuration/time.Second) + + assert.Less(t, attemptsPerSecond, int64(50)) +} + +func (h *SimpleServerHandler) ErrChanSub(ctx context.Context) (<-chan int, error) { + return nil, errors.New("expect to return an error") +} + +func TestRPCBadConnection(t *testing.T) { + // setup server + + serverHandler := &SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Add func(int) error + AddGet func(int) int + StringMatch func(t TestType, i2 int64) (out TestOut, err error) + ErrChanSub func(context.Context) (<-chan int, error) + } + closer, err := NewClient(context.Background(), "http://"+testServ.Listener.Addr().String()+"0", "SimpleServerHandler", &client, nil) + require.NoError(t, err) + err = client.Add(2) + require.True(t, errors.As(err, new(*RPCConnectionError))) + + defer closer() + +} + +func TestRPC(t *testing.T) { + // setup server + + serverHandler := &SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Add func(int) error + AddGet func(int) int + StringMatch func(t TestType, i2 int64) (out TestOut, err error) + ErrChanSub func(context.Context) (<-chan int, error) + } + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil) + require.NoError(t, err) + defer closer() + + // Add(int) error + + require.NoError(t, client.Add(2)) + require.Equal(t, 2, int(serverHandler.n)) + + err = client.Add(-3546) + require.EqualError(t, err, "test") + + // AddGet(int) int + + n := client.AddGet(3) + require.Equal(t, 5, n) + require.Equal(t, 5, int(serverHandler.n)) + + // StringMatch + + o, err := client.StringMatch(TestType{S: "0"}, 0) + require.NoError(t, err) + require.Equal(t, "0", o.S) + require.Equal(t, 0, o.I) + + _, err = client.StringMatch(TestType{S: "5"}, 5) + require.EqualError(t, err, ":(") + + o, err = client.StringMatch(TestType{S: "8", I: 8}, 8) + require.NoError(t, err) + require.Equal(t, "8", o.S) + require.Equal(t, 8, o.I) + + // ErrChanSub + ctx := context.TODO() + _, err = client.ErrChanSub(ctx) + if err == nil { + t.Fatal("expect an err return, but got nil") + } + + // Invalid client handlers + + var noret struct { + Add func(int) + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret, nil) + require.NoError(t, err) + + // this one should actually work + noret.Add(4) + require.Equal(t, 9, int(serverHandler.n)) + closer() + + var noparam struct { + Add func() + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam, nil) + require.NoError(t, err) + + // shouldn't panic + noparam.Add() + closer() + + var erronly struct { + AddGet func() (int, error) + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly, nil) + require.NoError(t, err) + + _, err = erronly.AddGet() + if err == nil || err.Error() != "RPC error (-32602): wrong param count (method 'SimpleServerHandler.AddGet'): 0 != 1" { + t.Error("wrong error:", err) + } + closer() + + var wrongtype struct { + Add func(string) error + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype, nil) + require.NoError(t, err) + + err = wrongtype.Add("not an int") + if err == nil || !strings.Contains(err.Error(), "RPC error (-32700):") || !strings.Contains(err.Error(), "json: cannot unmarshal string into Go value of type int") { + t.Error("wrong error:", err) + } + closer() + + var notfound struct { + NotThere func(string) error + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", ¬found, nil) + require.NoError(t, err) + + err = notfound.NotThere("hello?") + if err == nil || err.Error() != "RPC error (-32601): method 'SimpleServerHandler.NotThere' not found" { + t.Error("wrong error:", err) + } + closer() +} + +func TestRPCHttpClient(t *testing.T) { + // setup server + + serverHandler := &SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Add func(int) error + AddGet func(int) int + StringMatch func(t TestType, i2 int64) (out TestOut, err error) + } + closer, err := NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil) + require.NoError(t, err) + defer closer() + + // Add(int) error + + require.NoError(t, client.Add(2)) + require.Equal(t, 2, int(serverHandler.n)) + + err = client.Add(-3546) + require.EqualError(t, err, "test") + + // AddGet(int) int + + n := client.AddGet(3) + require.Equal(t, 5, n) + require.Equal(t, 5, int(serverHandler.n)) + + // StringMatch + + o, err := client.StringMatch(TestType{S: "0"}, 0) + require.NoError(t, err) + require.Equal(t, "0", o.S) + require.Equal(t, 0, o.I) + + _, err = client.StringMatch(TestType{S: "5"}, 5) + require.EqualError(t, err, ":(") + + o, err = client.StringMatch(TestType{S: "8", I: 8}, 8) + require.NoError(t, err) + require.Equal(t, "8", o.S) + require.Equal(t, 8, o.I) + + // Invalid client handlers + + var noret struct { + Add func(int) + } + closer, err = NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret, nil) + require.NoError(t, err) + + // this one should actually work + noret.Add(4) + require.Equal(t, 9, int(serverHandler.n)) + closer() + + var noparam struct { + Add func() + } + closer, err = NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam, nil) + require.NoError(t, err) + + // shouldn't panic + noparam.Add() + closer() + + var erronly struct { + AddGet func() (int, error) + } + closer, err = NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly, nil) + require.NoError(t, err) + + _, err = erronly.AddGet() + if err == nil || err.Error() != "RPC error (-32602): wrong param count (method 'SimpleServerHandler.AddGet'): 0 != 1" { + t.Error("wrong error:", err) + } + closer() + + var wrongtype struct { + Add func(string) error + } + closer, err = NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype, nil) + require.NoError(t, err) + + err = wrongtype.Add("not an int") + if err == nil || !strings.Contains(err.Error(), "RPC error (-32700):") || !strings.Contains(err.Error(), "json: cannot unmarshal string into Go value of type int") { + t.Error("wrong error:", err) + } + closer() + + var notfound struct { + NotThere func(string) error + } + closer, err = NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "SimpleServerHandler", ¬found, nil) + require.NoError(t, err) + + err = notfound.NotThere("hello?") + if err == nil || err.Error() != "RPC error (-32601): method 'SimpleServerHandler.NotThere' not found" { + t.Error("wrong error:", err) + } + closer() +} + +func TestParallelRPC(t *testing.T) { + // setup server + + serverHandler := &SimpleServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Add func(int) error + } + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil) + require.NoError(t, err) + defer closer() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + require.NoError(t, client.Add(2)) + } + }() + } + wg.Wait() + + require.Equal(t, 20000, int(serverHandler.n)) +} + +type CtxHandler struct { + lk sync.Mutex + + cancelled bool + i int + connectionType ConnectionType +} + +func (h *CtxHandler) Test(ctx context.Context) { + h.lk.Lock() + defer h.lk.Unlock() + timeout := time.After(300 * time.Millisecond) + h.i++ + h.connectionType = GetConnectionType(ctx) + + select { + case <-timeout: + case <-ctx.Done(): + h.cancelled = true + } +} + +func TestCtx(t *testing.T) { + // setup server + + serverHandler := &CtxHandler{} + + rpcServer := NewServer() + rpcServer.Register("CtxHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Test func(ctx context.Context) + } + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "CtxHandler", &client, nil) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + client.Test(ctx) + serverHandler.lk.Lock() + + if !serverHandler.cancelled { + t.Error("expected cancellation on the server side") + } + if serverHandler.connectionType != ConnectionTypeWS { + t.Error("wrong connection type") + } + + serverHandler.cancelled = false + + serverHandler.lk.Unlock() + closer() + + var noCtxClient struct { + Test func() + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient, nil) + if err != nil { + t.Fatal(err) + } + + noCtxClient.Test() + + serverHandler.lk.Lock() + + if serverHandler.cancelled || serverHandler.i != 2 { + t.Error("wrong serverHandler state") + } + if serverHandler.connectionType != ConnectionTypeWS { + t.Error("wrong connection type") + } + + serverHandler.lk.Unlock() + closer() +} + +func TestCtxHttp(t *testing.T) { + // setup server + + serverHandler := &CtxHandler{} + + rpcServer := NewServer() + rpcServer.Register("CtxHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Test func(ctx context.Context) + } + closer, err := NewClient(context.Background(), "http://"+testServ.Listener.Addr().String(), "CtxHandler", &client, nil) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + client.Test(ctx) + serverHandler.lk.Lock() + + if !serverHandler.cancelled { + t.Error("expected cancellation on the server side") + } + if serverHandler.connectionType != ConnectionTypeHTTP { + t.Error("wrong connection type") + } + + serverHandler.cancelled = false + + serverHandler.lk.Unlock() + closer() + + var noCtxClient struct { + Test func() + } + closer, err = NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient, nil) + if err != nil { + t.Fatal(err) + } + + noCtxClient.Test() + + serverHandler.lk.Lock() + + if serverHandler.cancelled || serverHandler.i != 2 { + t.Error("wrong serverHandler state") + } + // connection type should have switched to WS + if serverHandler.connectionType != ConnectionTypeWS { + t.Error("wrong connection type") + } + + serverHandler.lk.Unlock() + closer() +} + +type UnUnmarshalable int + +func (*UnUnmarshalable) UnmarshalJSON([]byte) error { + return errors.New("nope") +} + +type UnUnmarshalableHandler struct{} + +func (*UnUnmarshalableHandler) GetUnUnmarshalableStuff() (UnUnmarshalable, error) { + return UnUnmarshalable(5), nil +} + +func TestUnmarshalableResult(t *testing.T) { + var client struct { + GetUnUnmarshalableStuff func() (UnUnmarshalable, error) + } + + rpcServer := NewServer() + rpcServer.Register("Handler", &UnUnmarshalableHandler{}) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Handler", &client, nil) + require.NoError(t, err) + defer closer() + + _, err = client.GetUnUnmarshalableStuff() + require.EqualError(t, err, "RPC client error: unmarshaling result: nope") +} + +type ChanHandler struct { + wait chan struct{} + ctxdone <-chan struct{} +} + +func (h *ChanHandler) Sub(ctx context.Context, i int, eq int) (<-chan int, error) { + out := make(chan int) + h.ctxdone = ctx.Done() + + wait := h.wait + + log.Warnf("SERVER SUB!") + go func() { + defer close(out) + var n int + + for { + select { + case <-ctx.Done(): + fmt.Println("ctxdone1", i, eq) + return + case <-wait: + //fmt.Println("CONSUMED WAIT: ", i) + } + + n += i + + if n == eq { + fmt.Println("eq") + return + } + + select { + case <-ctx.Done(): + fmt.Println("ctxdone2") + return + case out <- n: + } + } + }() + + return out, nil +} + +func TestChan(t *testing.T) { + var client struct { + Sub func(context.Context, int, int) (<-chan int, error) + } + + serverHandler := &ChanHandler{ + wait: make(chan struct{}, 5), + } + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil) + require.NoError(t, err) + + defer closer() + + serverHandler.wait <- struct{}{} + + ctx, cancel := context.WithCancel(context.Background()) + + // sub + + sub, err := client.Sub(ctx, 2, -1) + require.NoError(t, err) + + // recv one + + require.Equal(t, 2, <-sub) + + // recv many (order) + + serverHandler.wait <- struct{}{} + serverHandler.wait <- struct{}{} + serverHandler.wait <- struct{}{} + + require.Equal(t, 4, <-sub) + require.Equal(t, 6, <-sub) + require.Equal(t, 8, <-sub) + + // close (through ctx) + cancel() + + _, ok := <-sub + require.Equal(t, false, ok) + + // sub (again) + + serverHandler.wait = make(chan struct{}, 5) + serverHandler.wait <- struct{}{} + + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + + log.Warnf("last sub") + sub, err = client.Sub(ctx, 3, 6) + require.NoError(t, err) + + log.Warnf("waiting for value now") + require.Equal(t, 3, <-sub) + log.Warnf("not equal") + + // close (remote) + serverHandler.wait <- struct{}{} + _, ok = <-sub + require.Equal(t, false, ok) +} + +func TestChanClosing(t *testing.T) { + var client struct { + Sub func(context.Context, int, int) (<-chan int, error) + } + + serverHandler := &ChanHandler{ + wait: make(chan struct{}, 5), + } + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil) + require.NoError(t, err) + + defer closer() + + ctx1, cancel1 := context.WithCancel(context.Background()) + ctx2, cancel2 := context.WithCancel(context.Background()) + + // sub + + sub1, err := client.Sub(ctx1, 2, -1) + require.NoError(t, err) + + sub2, err := client.Sub(ctx2, 3, -1) + require.NoError(t, err) + + // recv one + + serverHandler.wait <- struct{}{} + serverHandler.wait <- struct{}{} + + require.Equal(t, 2, <-sub1) + require.Equal(t, 3, <-sub2) + + cancel1() + + require.Equal(t, 0, <-sub1) + time.Sleep(time.Millisecond * 50) // make sure the loop has exited (having a shared wait channel makes this annoying) + + serverHandler.wait <- struct{}{} + require.Equal(t, 6, <-sub2) + + cancel2() + require.Equal(t, 0, <-sub2) +} + +func TestChanServerClose(t *testing.T) { + var client struct { + Sub func(context.Context, int, int) (<-chan int, error) + } + + serverHandler := &ChanHandler{ + wait: make(chan struct{}, 5), + } + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + tctx, tcancel := context.WithCancel(context.Background()) + + testServ := httptest.NewUnstartedServer(rpcServer) + testServ.Config.ConnContext = func(ctx context.Context, c net.Conn) context.Context { + return tctx + } + testServ.Start() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil) + require.NoError(t, err) + + defer closer() + + serverHandler.wait <- struct{}{} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // sub + + sub, err := client.Sub(ctx, 2, -1) + require.NoError(t, err) + + // recv one + + require.Equal(t, 2, <-sub) + + // make sure we're blocked + + select { + case <-time.After(200 * time.Millisecond): + case <-sub: + t.Fatal("didn't expect to get anything from sub") + } + + // close server + + tcancel() + testServ.Close() + + _, ok := <-sub + require.Equal(t, false, ok) +} + +func TestServerChanLockClose(t *testing.T) { + var client struct { + Sub func(context.Context, int, int) (<-chan int, error) + } + + serverHandler := &ChanHandler{ + wait: make(chan struct{}), + } + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + + var closeConn func() error + + _, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), + "ChanHandler", + []interface{}{&client}, nil, + func(c *Config) { + c.proxyConnFactory = func(f func() (*websocket.Conn, error)) func() (*websocket.Conn, error) { + return func() (*websocket.Conn, error) { + c, err := f() + if err != nil { + return nil, err + } + + closeConn = c.UnderlyingConn().Close + + return c, nil + } + } + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // sub + + sub, err := client.Sub(ctx, 2, -1) + require.NoError(t, err) + + // recv one + + go func() { + serverHandler.wait <- struct{}{} + }() + require.Equal(t, 2, <-sub) + + for i := 0; i < 100; i++ { + serverHandler.wait <- struct{}{} + } + + if err := closeConn(); err != nil { + t.Fatal(err) + } + + <-serverHandler.ctxdone +} + +type StreamingHandler struct { +} + +func (h *StreamingHandler) GetData(ctx context.Context, n int) (<-chan int, error) { + out := make(chan int) + + go func() { + defer close(out) + + for i := 0; i < n; i++ { + out <- i + } + }() + + return out, nil +} + +func TestChanClientReceiveAll(t *testing.T) { + var client struct { + GetData func(context.Context, int) (<-chan int, error) + } + + serverHandler := &StreamingHandler{} + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + tctx, tcancel := context.WithCancel(context.Background()) + + testServ := httptest.NewUnstartedServer(rpcServer) + testServ.Config.ConnContext = func(ctx context.Context, c net.Conn) context.Context { + return tctx + } + testServ.Start() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil) + require.NoError(t, err) + + defer closer() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // sub + + sub, err := client.GetData(ctx, 100) + require.NoError(t, err) + + for i := 0; i < 100; i++ { + select { + case v, ok := <-sub: + if !ok { + t.Fatal("channel closed", i) + } + + if v != i { + t.Fatal("got wrong value", v, i) + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for values") + } + } + + tcancel() + testServ.Close() + +} + +func TestControlChanDeadlock(t *testing.T) { + if _, exists := os.LookupEnv("GOLOG_LOG_LEVEL"); !exists { + _ = logging.SetLogLevel("rpc", "error") + defer func() { + _ = logging.SetLogLevel("rpc", "DEBUG") + }() + } + + for r := 0; r < 20; r++ { + testControlChanDeadlock(t) + } +} + +func testControlChanDeadlock(t *testing.T) { + var client struct { + Sub func(context.Context, int, int) (<-chan int, error) + } + + n := 5000 + + serverHandler := &ChanHandler{ + wait: make(chan struct{}, n), + } + + rpcServer := NewServer() + rpcServer.Register("ChanHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil) + require.NoError(t, err) + + defer closer() + + for i := 0; i < n; i++ { + serverHandler.wait <- struct{}{} + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sub, err := client.Sub(ctx, 1, -1) + require.NoError(t, err) + + done := make(chan struct{}) + + go func() { + defer close(done) + for i := 0; i < n; i++ { + if <-sub != i+1 { + panic("bad!") + // require.Equal(t, i+1, <-sub) + } + } + }() + + // reset this channel so its not shared between the sub requests... + serverHandler.wait = make(chan struct{}, n) + for i := 0; i < n; i++ { + serverHandler.wait <- struct{}{} + } + + _, err = client.Sub(ctx, 2, -1) + require.NoError(t, err) + <-done +} + +type InterfaceHandler struct { +} + +func (h *InterfaceHandler) ReadAll(ctx context.Context, r io.Reader) ([]byte, error) { + return io.ReadAll(r) +} + +func TestInterfaceHandler(t *testing.T) { + var client struct { + ReadAll func(ctx context.Context, r io.Reader) ([]byte, error) + } + + serverHandler := &InterfaceHandler{} + + rpcServer := NewServer(WithParamDecoder(new(io.Reader), readerDec)) + rpcServer.Register("InterfaceHandler", serverHandler) + + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "InterfaceHandler", []interface{}{&client}, nil, WithParamEncoder(new(io.Reader), readerEnc)) + require.NoError(t, err) + + defer closer() + + read, err := client.ReadAll(context.TODO(), strings.NewReader("pooooootato")) + require.NoError(t, err) + require.Equal(t, "pooooootato", string(read), "potatos weren't equal") +} + +var ( + readerRegistery = map[int]io.Reader{} + readerRegisteryN = 31 + readerRegisteryLk sync.Mutex +) + +func readerEnc(rin reflect.Value) (reflect.Value, error) { + reader := rin.Interface().(io.Reader) + + readerRegisteryLk.Lock() + defer readerRegisteryLk.Unlock() + + n := readerRegisteryN + readerRegisteryN++ + + readerRegistery[n] = reader + return reflect.ValueOf(n), nil +} + +func readerDec(ctx context.Context, rin []byte) (reflect.Value, error) { + var id int + if err := json.Unmarshal(rin, &id); err != nil { + return reflect.Value{}, err + } + + readerRegisteryLk.Lock() + defer readerRegisteryLk.Unlock() + + return reflect.ValueOf(readerRegistery[id]), nil +} + +type ErrSomethingBad struct{} + +func (e ErrSomethingBad) Error() string { + return "something bad has happened" +} + +type ErrMyErr struct{ str string } + +var _ error = ErrSomethingBad{} + +func (e *ErrMyErr) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &e.str) +} + +func (e *ErrMyErr) MarshalJSON() ([]byte, error) { + return json.Marshal(e.str) +} + +func (e *ErrMyErr) Error() string { + return fmt.Sprintf("this happened: %s", e.str) +} + +type ErrHandler struct{} + +func (h *ErrHandler) Test() error { + return ErrSomethingBad{} +} + +func (h *ErrHandler) TestP() error { + return &ErrSomethingBad{} +} + +func (h *ErrHandler) TestMy(s string) error { + return &ErrMyErr{ + str: s, + } +} + +func TestUserError(t *testing.T) { + // setup server + + serverHandler := &ErrHandler{} + + const ( + EBad = iota + FirstUserCode + EBad2 + EMy + ) + + errs := NewErrors() + errs.Register(EBad, new(ErrSomethingBad)) + errs.Register(EBad2, new(*ErrSomethingBad)) + errs.Register(EMy, new(*ErrMyErr)) + + rpcServer := NewServer(WithServerErrors(errs)) + rpcServer.Register("ErrHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Test func() error + TestP func() error + TestMy func(s string) error + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ErrHandler", []interface{}{ + &client, + }, nil, WithErrors(errs)) + require.NoError(t, err) + + e := client.Test() + require.True(t, xerrors.Is(e, ErrSomethingBad{})) + + e = client.TestP() + require.True(t, xerrors.Is(e, &ErrSomethingBad{})) + + e = client.TestMy("some event") + require.Error(t, e) + require.Equal(t, "this happened: some event", e.Error()) + require.Equal(t, "this happened: some event", e.(*ErrMyErr).Error()) + + closer() +} + +// Unit test for request/response ID translation. +func TestIDHandling(t *testing.T) { + var decoded request + + cases := []struct { + str string + expect interface{} + expectErr bool + }{ + { + `{"id":"8116d306-56cc-4637-9dd7-39ce1548a5a0","jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, + "8116d306-56cc-4637-9dd7-39ce1548a5a0", + false, + }, + {`{"id":1234,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, float64(1234), false}, + {`{"id":null,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, nil, false}, + {`{"id":1234.0,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, 1234.0, false}, + {`{"id":1.2,"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, 1.2, false}, + {`{"id":["1"],"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, nil, true}, + {`{"id":{"a":"b"},"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}`, nil, true}, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%v", tc.expect), func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(tc.str)) + require.NoError(t, dec.Decode(&decoded)) + if id, err := normalizeID(decoded.ID); !tc.expectErr { + require.NoError(t, err) + require.Equal(t, tc.expect, id) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAliasedCall(t *testing.T) { + // setup server + + rpcServer := NewServer() + rpcServer.Register("ServName", &SimpleServerHandler{n: 3}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + var client struct { + WhateverMethodName func(int) (int, error) `rpc_method:"ServName.AddGet"` + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{ + &client, + }, nil) + require.NoError(t, err) + + // do the call! + + n, err := client.WhateverMethodName(1) + require.NoError(t, err) + + require.Equal(t, 4, n) + + closer() +} + +type NotifHandler struct { + notified chan struct{} +} + +func (h *NotifHandler) Notif() { + close(h.notified) +} + +func TestNotif(t *testing.T) { + tc := func(proto string) func(t *testing.T) { + return func(t *testing.T) { + // setup server + + nh := &NotifHandler{ + notified: make(chan struct{}), + } + + rpcServer := NewServer() + rpcServer.Register("Notif", nh) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + var client struct { + Notif func() error `notify:"true"` + } + closer, err := NewMergeClient(context.Background(), proto+"://"+testServ.Listener.Addr().String(), "Notif", []interface{}{ + &client, + }, nil) + require.NoError(t, err) + + // do the call! + + // this will block if it's not sent as a notification + err = client.Notif() + require.NoError(t, err) + + <-nh.notified + + closer() + } + } + + t.Run("ws", tc("ws")) + t.Run("http", tc("http")) +} + +type RawParamHandler struct { +} + +type CustomParams struct { + I int +} + +func (h *RawParamHandler) Call(ctx context.Context, ps RawParams) (int, error) { + p, err := DecodeParams[CustomParams](ps) + if err != nil { + return 0, err + } + return p.I + 1, nil +} + +func TestCallWithRawParams(t *testing.T) { + // setup server + + rpcServer := NewServer() + rpcServer.Register("Raw", &RawParamHandler{}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + var client struct { + Call func(ctx context.Context, ps RawParams) (int, error) + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Raw", []interface{}{ + &client, + }, nil) + require.NoError(t, err) + + // do the call! + + // this will block if it's not sent as a notification + n, err := client.Call(context.Background(), []byte(`{"I": 1}`)) + require.NoError(t, err) + require.Equal(t, 2, n) + + closer() +} + +type RevCallTestServerHandler struct { +} + +func (h *RevCallTestServerHandler) Call(ctx context.Context) error { + revClient, ok := ExtractReverseClient[RevCallTestClientProxy](ctx) + if !ok { + return fmt.Errorf("no reverse client") + } + + r, err := revClient.CallOnClient(7) // multiply by 2 on client + if err != nil { + return xerrors.Errorf("call on client: %w", err) + } + + if r != 14 { + return fmt.Errorf("unexpected result: %d", r) + } + + return nil +} + +type RevCallTestClientProxy struct { + CallOnClient func(int) (int, error) +} + +type RevCallTestClientHandler struct { +} + +func (h *RevCallTestClientHandler) CallOnClient(a int) (int, error) { + return a * 2, nil +} + +func TestReverseCall(t *testing.T) { + // setup server + + rpcServer := NewServer(WithReverseClient[RevCallTestClientProxy]("Client")) + rpcServer.Register("Server", &RevCallTestServerHandler{}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Call func() error + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{ + &client, + }, nil, WithClientHandler("Client", &RevCallTestClientHandler{})) + require.NoError(t, err) + + // do the call! + + e := client.Call() + require.NoError(t, e) + + closer() +} + +type RevCallTestServerHandlerAliased struct { +} + +func (h *RevCallTestServerHandlerAliased) Call(ctx context.Context) error { + revClient, ok := ExtractReverseClient[RevCallTestClientProxyAliased](ctx) + if !ok { + return fmt.Errorf("no reverse client") + } + + r, err := revClient.CallOnClient(8) // multiply by 2 on client + if err != nil { + return xerrors.Errorf("call on client: %w", err) + } + + if r != 16 { + return fmt.Errorf("unexpected result: %d", r) + } + + return nil +} + +type RevCallTestClientProxyAliased struct { + CallOnClient func(int) (int, error) `rpc_method:"rpc_thing"` +} + +func TestReverseCallAliased(t *testing.T) { + // setup server + + rpcServer := NewServer(WithReverseClient[RevCallTestClientProxyAliased]("Client")) + rpcServer.Register("Server", &RevCallTestServerHandlerAliased{}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Call func() error + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{ + &client, + }, nil, WithClientHandler("Client", &RevCallTestClientHandler{}), WithClientHandlerAlias("rpc_thing", "Client.CallOnClient")) + require.NoError(t, err) + + // do the call! + + e := client.Call() + require.NoError(t, e) + + closer() +} + +// RevCallDropTestServerHandler attempts to make a client call on a closed connection. +type RevCallDropTestServerHandler struct { + closeConn func() + res chan error +} + +func (h *RevCallDropTestServerHandler) Call(ctx context.Context) error { + revClient, ok := ExtractReverseClient[RevCallTestClientProxy](ctx) + if !ok { + return fmt.Errorf("no reverse client") + } + + h.closeConn() + time.Sleep(time.Second) + + _, err := revClient.CallOnClient(7) + h.res <- err + + return nil +} + +func TestReverseCallDroppedConn(t *testing.T) { + // setup server + + hnd := &RevCallDropTestServerHandler{ + res: make(chan error), + } + + rpcServer := NewServer(WithReverseClient[RevCallTestClientProxy]("Client")) + rpcServer.Register("Server", hnd) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Call func() error + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{ + &client, + }, nil, WithClientHandler("Client", &RevCallTestClientHandler{})) + require.NoError(t, err) + + hnd.closeConn = closer + + // do the call! + e := client.Call() + + require.Error(t, e) + require.Contains(t, e.Error(), "websocket connection closed") + + res := <-hnd.res + require.Error(t, res) + require.Contains(t, res.Error(), "RPC client error: sendRequest failed: websocket routine exiting") + time.Sleep(100 * time.Millisecond) +} + +type BigCallTestServerHandler struct { +} + +type RecRes struct { + I int + R []RecRes +} + +func (h *BigCallTestServerHandler) Do() (RecRes, error) { + var res RecRes + res.I = 123 + + for i := 0; i < 15000; i++ { + var ires RecRes + ires.I = i + + for j := 0; j < 15000; j++ { + var jres RecRes + jres.I = j + + ires.R = append(ires.R, jres) + } + + res.R = append(res.R, ires) + } + + fmt.Println("sending result") + + return res, nil +} + +func (h *BigCallTestServerHandler) Ch(ctx context.Context) (<-chan int, error) { + out := make(chan int) + + go func() { + var i int + for { + select { + case <-ctx.Done(): + fmt.Println("closing") + close(out) + return + case <-time.After(time.Second): + } + fmt.Println("sending") + out <- i + i++ + } + }() + + return out, nil +} + +// TestBigResult tests that the connection doesn't die when sending a large result, +// and that requests which happen while a large result is being sent don't fail. +func TestBigResult(t *testing.T) { + if os.Getenv("I_HAVE_A_LOT_OF_MEMORY_AND_TIME") != "1" { + // needs ~40GB of memory and ~4 minutes to run + t.Skip("skipping test due to required resources, set I_HAVE_A_LOT_OF_MEMORY_AND_TIME=1 to run") + } + + // setup server + + serverHandler := &BigCallTestServerHandler{} + + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + // setup client + + var client struct { + Do func() (RecRes, error) + Ch func(ctx context.Context) (<-chan int, error) + } + closer, err := NewClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil) + require.NoError(t, err) + defer closer() + + chctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // client.Ch will generate some requests, which will require websocket locks, + // and before fixes in #97 would cause deadlocks / timeouts when combined with + // the large result processing from client.Do + ch, err := client.Ch(chctx) + require.NoError(t, err) + + prevN := <-ch + + go func() { + for n := range ch { + if n != prevN+1 { + panic("bad order") + } + prevN = n + } + }() + + _, err = client.Do() + require.NoError(t, err) + + fmt.Println("done") +} + +func TestNewCustomClient(t *testing.T) { + // Setup server + serverHandler := &SimpleServerHandler{} + rpcServer := NewServer() + rpcServer.Register("SimpleServerHandler", serverHandler) + + // Custom doRequest function + doRequest := func(ctx context.Context, body []byte) (io.ReadCloser, error) { + reader := bytes.NewReader(body) + pr, pw := io.Pipe() + go func() { + defer pw.Close() + rpcServer.HandleRequest(ctx, reader, pw) + }() + return pr, nil + } + + var client struct { + Add func(int) error + AddGet func(int) int + } + + // Create custom client + closer, err := NewCustomClient("SimpleServerHandler", []interface{}{&client}, doRequest) + require.NoError(t, err) + defer closer() + + // Add(int) error + require.NoError(t, client.Add(10)) + require.Equal(t, int32(10), serverHandler.n) + + err = client.Add(-3546) + require.EqualError(t, err, "test") + + // AddGet(int) int + n := client.AddGet(3) + require.Equal(t, 13, n) + require.Equal(t, int32(13), serverHandler.n) +} + +func TestReverseCallWithCustomMethodName(t *testing.T) { + // setup server + + rpcServer := NewServer(WithServerMethodNameFormatter(func(namespace, method string) string { return namespace + "_" + method })) + rpcServer.Register("Server", &RawParamHandler{}) + + // httptest stuff + testServ := httptest.NewServer(rpcServer) + defer testServ.Close() + + // setup client + + var client struct { + Call func(ctx context.Context, ps RawParams) error `rpc_method:"Server_Call"` + } + closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "Server", []interface{}{ + &client, + }, nil) + require.NoError(t, err) + + // do the call! + + e := client.Call(context.Background(), []byte(`{"I": 1}`)) + require.NoError(t, e) + + closer() +} + +type MethodTransformedHandler struct{} + +func (h *RawParamHandler) CallSomethingInSnakeCase(ctx context.Context, v int) (int, error) { + return v + 1, nil +} diff --git a/common/serialize/go-jsonrpc/server.go b/common/serialize/go-jsonrpc/server.go new file mode 100644 index 000000000..4454c8500 --- /dev/null +++ b/common/serialize/go-jsonrpc/server.go @@ -0,0 +1,183 @@ +package jsonrpc + +import ( + "context" + "encoding/json" + "io" + "net/http" + "runtime/pprof" + "strings" + "time" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +const ( + rpcParseError = -32700 + rpcInvalidRequest = -32600 + rpcMethodNotFound = -32601 + rpcInvalidParams = -32602 +) + +// ConnectionType indicates the type of connection, this is set in the context and can be retrieved +// with GetConnectionType. +type ConnectionType string + +const ( + // ConnectionTypeUnknown indicates that the connection type cannot be determined, likely because + // it hasn't passed through an RPCServer. + ConnectionTypeUnknown ConnectionType = "unknown" + // ConnectionTypeHTTP indicates that the connection is an HTTP connection. + ConnectionTypeHTTP ConnectionType = "http" + // ConnectionTypeWS indicates that the connection is a WebSockets connection. + ConnectionTypeWS ConnectionType = "websockets" +) + +var connectionTypeCtxKey = &struct{ name string }{"jsonrpc-connection-type"} + +// GetConnectionType returns the connection type of the request if it was set by an RPCServer. +// A connection type of ConnectionTypeUnknown means that the connection type was not set. +func GetConnectionType(ctx context.Context) ConnectionType { + if v := ctx.Value(connectionTypeCtxKey); v != nil { + return v.(ConnectionType) + } + return ConnectionTypeUnknown +} + +// RPCServer provides a jsonrpc 2.0 http server handler +type RPCServer struct { + *handler + reverseClientBuilder func(context.Context, *wsConn) (context.Context, error) + + pingInterval time.Duration +} + +// NewServer creates new RPCServer instance +func NewServer(opts ...ServerOption) *RPCServer { + config := defaultServerConfig() + for _, o := range opts { + o(&config) + } + + return &RPCServer{ + handler: makeHandler(config), + reverseClientBuilder: config.reverseClientBuilder, + + pingInterval: config.pingInterval, + } +} + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (s *RPCServer) handleWS(ctx context.Context, w http.ResponseWriter, r *http.Request) { + // TODO: allow setting + // (note that we still are mostly covered by jwt tokens) + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Header.Get("Sec-WebSocket-Protocol") != "" { + w.Header().Set("Sec-WebSocket-Protocol", r.Header.Get("Sec-WebSocket-Protocol")) + } + + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Errorw("upgrading connection", "error", err) + // note that upgrader.Upgrade will set http error if there is an error + return + } + + wc := &wsConn{ + conn: c, + handler: s, + pingInterval: s.pingInterval, + exiting: make(chan struct{}), + } + + if s.reverseClientBuilder != nil { + ctx, err = s.reverseClientBuilder(ctx, wc) + if err != nil { + log.Errorf("failed to build reverse client: %s", err) + w.WriteHeader(500) + return + } + } + + lbl := pprof.Labels("jrpc-mode", "wsserver", "jrpc-remote", r.RemoteAddr, "jrpc-uuid", uuid.New().String()) + pprof.Do(ctx, lbl, func(ctx context.Context) { + wc.handleWsConn(ctx) + }) + + if err := c.Close(); err != nil { + log.Errorw("closing websocket connection", "error", err) + return + } +} + +// TODO: return errors to clients per spec +func (s *RPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + h := strings.ToLower(r.Header.Get("Connection")) + if strings.Contains(h, "upgrade") { + ctx = context.WithValue(ctx, connectionTypeCtxKey, ConnectionTypeWS) + s.handleWS(ctx, w, r) + return + } + + ctx = context.WithValue(ctx, connectionTypeCtxKey, ConnectionTypeHTTP) + s.handleReader(ctx, r.Body, w, rpcError) +} + +func (s *RPCServer) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) { + s.handleReader(ctx, r, w, rpcError) +} + +func rpcError(wf func(func(io.Writer)), req *request, code ErrorCode, err error) { + log.Errorf("RPC Error: %s", err) + wf(func(w io.Writer) { + if hw, ok := w.(http.ResponseWriter); ok { + if code == rpcInvalidRequest { + hw.WriteHeader(http.StatusBadRequest) + } else { + hw.WriteHeader(http.StatusInternalServerError) + } + } + + log.Warnf("rpc error: %s", err) + + if req == nil { + req = &request{} + } + + resp := response{ + Jsonrpc: "2.0", + ID: req.ID, + Error: &JSONRPCError{ + Code: code, + Message: err.Error(), + }, + } + + err = json.NewEncoder(w).Encode(resp) + if err != nil { + log.Warnf("failed to write rpc error: %s", err) + return + } + }) +} + +// Register registers new RPC handler +// +// Handler is any value with methods defined +func (s *RPCServer) Register(namespace string, handler interface{}) { + s.register(namespace, handler) +} + +func (s *RPCServer) AliasMethod(alias, original string) { + s.aliasedMethods[alias] = original +} + +var _ error = &JSONRPCError{} diff --git a/common/serialize/go-jsonrpc/util.go b/common/serialize/go-jsonrpc/util.go new file mode 100644 index 000000000..09fc39363 --- /dev/null +++ b/common/serialize/go-jsonrpc/util.go @@ -0,0 +1,81 @@ +package jsonrpc + +import ( + "encoding/json" + "fmt" + "math" + "math/rand" + "reflect" + "time" +) + +type param struct { + data []byte // from unmarshal + + v reflect.Value // to marshal +} + +func (p *param) UnmarshalJSON(raw []byte) error { + p.data = make([]byte, len(raw)) + copy(p.data, raw) + return nil +} + +func (p *param) MarshalJSON() ([]byte, error) { + if p.v.Kind() == reflect.Invalid { + return p.data, nil + } + + return json.Marshal(p.v.Interface()) +} + +// processFuncOut finds value and error Outs in function +func processFuncOut(funcType reflect.Type) (valOut int, errOut int, n int) { + errOut = -1 // -1 if not found + valOut = -1 + n = funcType.NumOut() + + switch n { + case 0: + case 1: + if funcType.Out(0) == errorType { + errOut = 0 + } else { + valOut = 0 + } + case 2: + valOut = 0 + errOut = 1 + if funcType.Out(1) != errorType { + panic("expected error as second return value") + } + default: + errstr := fmt.Sprintf("too many return values: %s", funcType) + panic(errstr) + } + + return +} + +type backoff struct { + minDelay time.Duration + maxDelay time.Duration +} + +func (b *backoff) next(attempt int) time.Duration { + if attempt < 0 { + return b.minDelay + } + + minf := float64(b.minDelay) + durf := minf * math.Pow(1.5, float64(attempt)) + durf = durf + rand.Float64()*minf + + delay := time.Duration(durf) + + if delay > b.maxDelay { + return b.maxDelay + } + + return delay +} diff --git a/common/serialize/go-jsonrpc/version.json b/common/serialize/go-jsonrpc/version.json new file mode 100644 index 000000000..0ad79e3bf --- /dev/null +++ b/common/serialize/go-jsonrpc/version.json @@ -0,0 +1,3 @@ +{ + "version": "v0.8.0" +} diff --git a/common/serialize/go-jsonrpc/websocket.go b/common/serialize/go-jsonrpc/websocket.go new file mode 100644 index 000000000..1ab117b71 --- /dev/null +++ b/common/serialize/go-jsonrpc/websocket.go @@ -0,0 +1,940 @@ +package jsonrpc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "reflect" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" + "golang.org/x/xerrors" +) + +const wsCancel = "xrpc.cancel" +const chValue = "xrpc.ch.val" +const chClose = "xrpc.ch.close" + +var debugTrace = os.Getenv("JSONRPC_ENABLE_DEBUG_TRACE") == "1" + +type frame struct { + // common + Jsonrpc string `json:"jsonrpc"` + ID interface{} `json:"id,omitempty"` + Meta map[string]string `json:"meta,omitempty"` + + // request + Method string `json:"method,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + + // response + Result json.RawMessage `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` +} + +type outChanReg struct { + reqID interface{} + + chID uint64 + ch reflect.Value +} + +type reqestHandler interface { + handle(ctx context.Context, req request, w func(func(io.Writer)), rpcError rpcErrFunc, done func(keepCtx bool), chOut chanOut) +} + +type wsConn struct { + // outside params + conn *websocket.Conn + connFactory func() (*websocket.Conn, error) + reconnectBackoff backoff + pingInterval time.Duration + timeout time.Duration + handler reqestHandler + requests <-chan clientRequest + pongs chan struct{} + stopPings func() + stop <-chan struct{} + exiting chan struct{} + + // incoming messages + incoming chan io.Reader + incomingErr error + errLk sync.Mutex + + readError chan error + + frameExecQueue chan []byte + + // outgoing messages + writeLk sync.Mutex + + // //// + // Client related + + // inflight are requests we've sent to the remote + inflight map[interface{}]clientRequest + inflightLk sync.Mutex + + // chanHandlers is a map of client-side channel handlers + chanHandlersLk sync.Mutex + chanHandlers map[uint64]*chanHandler + + // //// + // Server related + + // handling are the calls we handle + handling map[interface{}]context.CancelFunc + handlingLk sync.Mutex + + spawnOutChanHandlerOnce sync.Once + + // chanCtr is a counter used for identifying output channels on the server side + chanCtr uint64 + + registerCh chan outChanReg +} + +type chanHandler struct { + // take inside chanHandlersLk + lk sync.Mutex + + cb func(m []byte, ok bool) +} + +// // +// WebSocket Message utils // +// // + +// nextMessage wait for one message and puts it to the incoming channel +func (c *wsConn) nextMessage() { + c.resetReadDeadline() + msgType, r, err := c.conn.NextReader() + if err != nil { + c.errLk.Lock() + c.incomingErr = err + c.errLk.Unlock() + close(c.incoming) + return + } + if msgType != websocket.BinaryMessage && msgType != websocket.TextMessage { + c.errLk.Lock() + c.incomingErr = errors.New("unsupported message type") + c.errLk.Unlock() + close(c.incoming) + return + } + c.incoming <- r +} + +// nextWriter waits for writeLk and invokes the cb callback with WS message +// writer when the lock is acquired +func (c *wsConn) nextWriter(cb func(io.Writer)) { + c.writeLk.Lock() + defer c.writeLk.Unlock() + + wcl, err := c.conn.NextWriter(websocket.TextMessage) + if err != nil { + log.Error("handle me:", err) + return + } + + cb(wcl) + + if err := wcl.Close(); err != nil { + log.Error("handle me:", err) + return + } +} + +func (c *wsConn) sendRequest(req request) error { + c.writeLk.Lock() + defer c.writeLk.Unlock() + + if debugTrace { + log.Debugw("sendRequest", "req", req.Method, "id", req.ID) + } + + if err := c.conn.WriteJSON(req); err != nil { + return err + } + return nil +} + +// // +// Output channels // +// // + +// handleOutChans handles channel communication on the server side +// (forwards channel messages to client) +func (c *wsConn) handleOutChans() { + regV := reflect.ValueOf(c.registerCh) + exitV := reflect.ValueOf(c.exiting) + + cases := []reflect.SelectCase{ + { // registration chan always 0 + Dir: reflect.SelectRecv, + Chan: regV, + }, + { // exit chan always 1 + Dir: reflect.SelectRecv, + Chan: exitV, + }, + } + internal := len(cases) + var caseToID []uint64 + + for { + chosen, val, ok := reflect.Select(cases) + + switch chosen { + case 0: // registration channel + if !ok { + // control channel closed - signals closed connection + // This shouldn't happen, instead the exiting channel should get closed + log.Warn("control channel closed") + return + } + + registration := val.Interface().(outChanReg) + + caseToID = append(caseToID, registration.chID) + cases = append(cases, reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: registration.ch, + }) + + c.nextWriter(func(w io.Writer) { + resp := &response{ + Jsonrpc: "2.0", + ID: registration.reqID, + Result: registration.chID, + } + + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Error(err) + return + } + }) + + continue + case 1: // exiting channel + if !ok { + // exiting channel closed - signals closed connection + // + // We're not closing any channels as we're on receiving end. + // Also, context cancellation below should take care of any running + // requests + return + } + log.Warn("exiting channel received a message") + continue + } + + if !ok { + // Output channel closed, cleanup, and tell remote that this happened + + id := caseToID[chosen-internal] + + n := len(cases) - 1 + if n > 0 { + cases[chosen] = cases[n] + caseToID[chosen-internal] = caseToID[n-internal] + } + + cases = cases[:n] + caseToID = caseToID[:n-internal] + + rp, err := json.Marshal([]param{{v: reflect.ValueOf(id)}}) + if err != nil { + log.Error(err) + continue + } + + if err := c.sendRequest(request{ + Jsonrpc: "2.0", + ID: nil, // notification + Method: chClose, + Params: rp, + }); err != nil { + log.Warnf("closed out channel sendRequest failed: %s", err) + } + continue + } + + // forward message + rp, err := json.Marshal([]param{{v: reflect.ValueOf(caseToID[chosen-internal])}, {v: val}}) + if err != nil { + log.Errorw("marshaling params for sendRequest failed", "err", err) + continue + } + + if err := c.sendRequest(request{ + Jsonrpc: "2.0", + ID: nil, // notification + Method: chValue, + Params: rp, + }); err != nil { + log.Warnf("sendRequest failed: %s", err) + return + } + } +} + +// handleChanOut registers output channel for forwarding to client +func (c *wsConn) handleChanOut(ch reflect.Value, req interface{}) error { + c.spawnOutChanHandlerOnce.Do(func() { + go c.handleOutChans() + }) + id := atomic.AddUint64(&c.chanCtr, 1) + + select { + case c.registerCh <- outChanReg{ + reqID: req, + + chID: id, + ch: ch, + }: + return nil + case <-c.exiting: + return xerrors.New("connection closing") + } +} + +// // +// Context.Done propagation // +// // + +// handleCtxAsync handles context lifetimes for client +// TODO: this should be aware of events going through chanHandlers, and quit +// +// when the related channel is closed. +// This should also probably be a single goroutine, +// Note that not doing this should be fine for now as long as we are using +// contexts correctly (cancelling when async functions are no longer is use) +func (c *wsConn) handleCtxAsync(actx context.Context, id interface{}) { + <-actx.Done() + + rp, err := json.Marshal([]param{{v: reflect.ValueOf(id)}}) + if err != nil { + log.Errorw("marshaling params for sendRequest failed", "err", err) + return + } + + if err := c.sendRequest(request{ + Jsonrpc: "2.0", + Method: wsCancel, + Params: rp, + }); err != nil { + log.Warnw("failed to send request", "method", wsCancel, "id", id, "error", err.Error()) + } +} + +// cancelCtx is a built-in rpc which handles context cancellation over rpc +func (c *wsConn) cancelCtx(req frame) { + if req.ID != nil { + log.Warnf("%s call with ID set, won't respond", wsCancel) + } + + var params []param + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + log.Error("failed to unmarshal channel id in xrpc.ch.val: %s", err) + return + } + + var id interface{} + if err := json.Unmarshal(params[0].data, &id); err != nil { + log.Error("handle me:", err) + return + } + + c.handlingLk.Lock() + defer c.handlingLk.Unlock() + + cf, ok := c.handling[id] + if ok { + cf() + } +} + +// // +// Main Handling logic // +// // + +func (c *wsConn) handleChanMessage(frame frame) { + var params []param + if err := json.Unmarshal(frame.Params, ¶ms); err != nil { + log.Error("failed to unmarshal channel id in xrpc.ch.val: %s", err) + return + } + + var chid uint64 + if err := json.Unmarshal(params[0].data, &chid); err != nil { + log.Error("failed to unmarshal channel id in xrpc.ch.val: %s", err) + return + } + + c.chanHandlersLk.Lock() + hnd, ok := c.chanHandlers[chid] + if !ok { + c.chanHandlersLk.Unlock() + log.Errorf("xrpc.ch.val: handler %d not found", chid) + return + } + + hnd.lk.Lock() + defer hnd.lk.Unlock() + + c.chanHandlersLk.Unlock() + + hnd.cb(params[1].data, true) +} + +func (c *wsConn) handleChanClose(frame frame) { + var params []param + if err := json.Unmarshal(frame.Params, ¶ms); err != nil { + log.Error("failed to unmarshal channel id in xrpc.ch.val: %s", err) + return + } + + var chid uint64 + if err := json.Unmarshal(params[0].data, &chid); err != nil { + log.Error("failed to unmarshal channel id in xrpc.ch.val: %s", err) + return + } + + c.chanHandlersLk.Lock() + hnd, ok := c.chanHandlers[chid] + if !ok { + c.chanHandlersLk.Unlock() + log.Errorf("xrpc.ch.val: handler %d not found", chid) + return + } + + hnd.lk.Lock() + defer hnd.lk.Unlock() + + delete(c.chanHandlers, chid) + + c.chanHandlersLk.Unlock() + + hnd.cb(nil, false) +} + +func (c *wsConn) handleResponse(frame frame) { + c.inflightLk.Lock() + req, ok := c.inflight[frame.ID] + c.inflightLk.Unlock() + if !ok { + log.Error("client got unknown ID in response") + return + } + + if req.retCh != nil && frame.Result != nil { + // output is channel + var chid uint64 + if err := json.Unmarshal(frame.Result, &chid); err != nil { + log.Errorf("failed to unmarshal channel id response: %s, data '%s'", err, string(frame.Result)) + return + } + + chanCtx, chHnd := req.retCh() + + c.chanHandlersLk.Lock() + c.chanHandlers[chid] = &chanHandler{cb: chHnd} + c.chanHandlersLk.Unlock() + + go c.handleCtxAsync(chanCtx, frame.ID) + } + + req.ready <- clientResponse{ + Jsonrpc: frame.Jsonrpc, + Result: frame.Result, + ID: frame.ID, + Error: frame.Error, + } + c.inflightLk.Lock() + delete(c.inflight, frame.ID) + c.inflightLk.Unlock() +} + +func (c *wsConn) handleCall(ctx context.Context, frame frame) { + if c.handler == nil { + log.Error("handleCall on client with no reverse handler") + return + } + + req := request{ + Jsonrpc: frame.Jsonrpc, + ID: frame.ID, + Meta: frame.Meta, + Method: frame.Method, + Params: frame.Params, + } + + ctx, cancel := context.WithCancel(ctx) + + nextWriter := func(cb func(io.Writer)) { + cb(io.Discard) + } + done := func(keepCtx bool) { + if !keepCtx { + cancel() + } + } + if frame.ID != nil { + nextWriter = c.nextWriter + + c.handlingLk.Lock() + c.handling[frame.ID] = cancel + c.handlingLk.Unlock() + + done = func(keepctx bool) { + c.handlingLk.Lock() + defer c.handlingLk.Unlock() + + if !keepctx { + cancel() + delete(c.handling, frame.ID) + } + } + } + + go c.handler.handle(ctx, req, nextWriter, rpcError, done, c.handleChanOut) +} + +// handleFrame handles all incoming messages (calls and responses) +func (c *wsConn) handleFrame(ctx context.Context, frame frame) { + // Get message type by method name: + // "" - response + // "xrpc.*" - builtin + // anything else - incoming remote call + switch frame.Method { + case "": // Response to our call + c.handleResponse(frame) + case wsCancel: + c.cancelCtx(frame) + case chValue: + c.handleChanMessage(frame) + case chClose: + c.handleChanClose(frame) + default: // Remote call + c.handleCall(ctx, frame) + } +} + +func (c *wsConn) closeInFlight() { + c.inflightLk.Lock() + for id, req := range c.inflight { + req.ready <- clientResponse{ + Jsonrpc: "2.0", + ID: id, + Error: &JSONRPCError{ + Message: "handler: websocket connection closed", + Code: eTempWSError, + }, + } + } + c.inflight = map[interface{}]clientRequest{} + c.inflightLk.Unlock() + + c.handlingLk.Lock() + for _, cancel := range c.handling { + cancel() + } + c.handling = map[interface{}]context.CancelFunc{} + c.handlingLk.Unlock() + +} + +func (c *wsConn) closeChans() { + c.chanHandlersLk.Lock() + defer c.chanHandlersLk.Unlock() + + for chid := range c.chanHandlers { + hnd := c.chanHandlers[chid] + + hnd.lk.Lock() + + delete(c.chanHandlers, chid) + + c.chanHandlersLk.Unlock() + + hnd.cb(nil, false) + + hnd.lk.Unlock() + c.chanHandlersLk.Lock() + } +} + +func (c *wsConn) setupPings() func() { + if c.pingInterval == 0 { + return func() {} + } + + c.conn.SetPongHandler(func(appData string) error { + select { + case c.pongs <- struct{}{}: + default: + } + return nil + }) + c.conn.SetPingHandler(func(appData string) error { + // treat pings as pongs - this lets us register server activity even if it's too busy to respond to our pings + select { + case c.pongs <- struct{}{}: + default: + } + return nil + }) + + stop := make(chan struct{}) + + go func() { + for { + select { + case <-time.After(c.pingInterval): + c.writeLk.Lock() + if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + log.Errorf("sending ping message: %+v", err) + } + c.writeLk.Unlock() + case <-stop: + return + } + } + }() + + var o sync.Once + return func() { + o.Do(func() { + close(stop) + }) + } +} + +// returns true if reconnected +func (c *wsConn) tryReconnect(ctx context.Context) bool { + if c.connFactory == nil { // server side + return false + } + + // connection dropped unexpectedly, do our best to recover it + c.closeInFlight() + c.closeChans() + c.incoming = make(chan io.Reader) // listen again for responses + go func() { + c.stopPings() + + attempts := 0 + var conn *websocket.Conn + for conn == nil { + time.Sleep(c.reconnectBackoff.next(attempts)) + if ctx.Err() != nil { + return + } + var err error + if conn, err = c.connFactory(); err != nil { + log.Debugw("websocket connection retry failed", "error", err) + } + select { + case <-ctx.Done(): + return + default: + } + attempts++ + } + + c.writeLk.Lock() + c.conn = conn + c.errLk.Lock() + c.incomingErr = nil + c.errLk.Unlock() + + c.stopPings = c.setupPings() + + c.writeLk.Unlock() + + go c.nextMessage() + }() + + return true +} + +func (c *wsConn) readFrame(ctx context.Context, r io.Reader) { + // debug util - dump all messages to stderr + // r = io.TeeReader(r, os.Stderr) + + // json.NewDecoder(r).Decode would read the whole frame as well, so might as well do it + // with ReadAll which should be much faster + // use a autoResetReader in case the read takes a long time + buf, err := io.ReadAll(c.autoResetReader(r)) // todo buffer pool + if err != nil { + c.readError <- xerrors.Errorf("reading frame into a buffer: %w", err) + return + } + + c.frameExecQueue <- buf + if len(c.frameExecQueue) > 2*cap(c.frameExecQueue)/3 { // warn at 2/3 capacity + log.Warnw("frame executor queue is backlogged", "queued", len(c.frameExecQueue), "cap", cap(c.frameExecQueue)) + } + + // got the whole frame, can start reading the next one in background + go c.nextMessage() +} + +func (c *wsConn) frameExecutor(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case buf := <-c.frameExecQueue: + var frame frame + if err := json.Unmarshal(buf, &frame); err != nil { + log.Warnw("failed to unmarshal frame", "error", err) + // todo send invalid request response + continue + } + + var err error + frame.ID, err = normalizeID(frame.ID) + if err != nil { + log.Warnw("failed to normalize frame id", "error", err) + // todo send invalid request response + continue + } + + c.handleFrame(ctx, frame) + } + } +} + +var maxQueuedFrames = 256 + +func (c *wsConn) handleWsConn(ctx context.Context) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + c.incoming = make(chan io.Reader) + c.readError = make(chan error, 1) + c.frameExecQueue = make(chan []byte, maxQueuedFrames) + c.inflight = map[interface{}]clientRequest{} + c.handling = map[interface{}]context.CancelFunc{} + c.chanHandlers = map[uint64]*chanHandler{} + c.pongs = make(chan struct{}, 1) + + c.registerCh = make(chan outChanReg) + defer close(c.exiting) + + // //// + + // on close, make sure to return from all pending calls, and cancel context + // on all calls we handle + defer c.closeInFlight() + defer c.closeChans() + + // setup pings + + c.stopPings = c.setupPings() + defer c.stopPings() + + var timeoutTimer *time.Timer + if c.timeout != 0 { + timeoutTimer = time.NewTimer(c.timeout) + defer timeoutTimer.Stop() + } + + // start frame executor + go c.frameExecutor(ctx) + + // wait for the first message + go c.nextMessage() + for { + var timeoutCh <-chan time.Time + if timeoutTimer != nil { + if !timeoutTimer.Stop() { + select { + case <-timeoutTimer.C: + default: + } + } + timeoutTimer.Reset(c.timeout) + + timeoutCh = timeoutTimer.C + } + + start := time.Now() + action := "" + + select { + case r, ok := <-c.incoming: + action = "incoming" + c.errLk.Lock() + err := c.incomingErr + c.errLk.Unlock() + + if ok { + go c.readFrame(ctx, r) + break + } + + if err == nil { + return // remote closed + } + + log.Debugw("websocket error", "error", err, "lastAction", action, "time", time.Since(start)) + // only client needs to reconnect + if !c.tryReconnect(ctx) { + return // failed to reconnect + } + case rerr := <-c.readError: + action = "read-error" + + log.Debugw("websocket error", "error", rerr, "lastAction", action, "time", time.Since(start)) + if !c.tryReconnect(ctx) { + return // failed to reconnect + } + case <-ctx.Done(): + log.Debugw("context cancelled", "error", ctx.Err(), "lastAction", action, "time", time.Since(start)) + return + case req := <-c.requests: + action = fmt.Sprintf("send-request(%s,%v)", req.req.Method, req.req.ID) + + c.writeLk.Lock() + if req.req.ID != nil { // non-notification + c.errLk.Lock() + hasErr := c.incomingErr != nil + c.errLk.Unlock() + if hasErr { // No conn?, immediate fail + req.ready <- clientResponse{ + Jsonrpc: "2.0", + ID: req.req.ID, + Error: &JSONRPCError{ + Message: "handler: websocket connection closed", + Code: eTempWSError, + }, + } + c.writeLk.Unlock() + break + } + c.inflightLk.Lock() + c.inflight[req.req.ID] = req + c.inflightLk.Unlock() + } + c.writeLk.Unlock() + serr := c.sendRequest(req.req) + if serr != nil { + log.Errorf("sendReqest failed (Handle me): %s", serr) + } + if req.req.ID == nil { // notification, return immediately + resp := clientResponse{ + Jsonrpc: "2.0", + } + if serr != nil { + resp.Error = &JSONRPCError{ + Code: eTempWSError, + Message: fmt.Sprintf("sendRequest: %s", serr), + } + } + req.ready <- resp + } + + case <-c.pongs: + action = "pong" + + c.resetReadDeadline() + case <-timeoutCh: + if c.pingInterval == 0 { + // pings not running, this is perfectly normal + continue + } + + c.writeLk.Lock() + if err := c.conn.Close(); err != nil { + log.Warnw("timed-out websocket close error", "error", err) + } + c.writeLk.Unlock() + log.Errorw("Connection timeout", "remote", c.conn.RemoteAddr(), "lastAction", action) + // The server side does not perform the reconnect operation, so need to exit + if c.connFactory == nil { + return + } + // The client performs the reconnect operation, and if it exits it cannot start a handleWsConn again, so it does not need to exit + continue + case <-c.stop: + c.writeLk.Lock() + cmsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := c.conn.WriteMessage(websocket.CloseMessage, cmsg); err != nil { + log.Warn("failed to write close message: ", err) + } + if err := c.conn.Close(); err != nil { + log.Warnw("websocket close error", "error", err) + } + c.writeLk.Unlock() + return + } + + if c.pingInterval > 0 && time.Since(start) > c.pingInterval*2 { + log.Warnw("websocket long time no response", "lastAction", action, "time", time.Since(start)) + } + if debugTrace { + log.Debugw("websocket action", "lastAction", action, "time", time.Since(start)) + } + } +} + +var onReadDeadlineResetInterval = 5 * time.Second + +// autoResetReader wraps a reader and resets the read deadline on if needed when doing large reads. +func (c *wsConn) autoResetReader(reader io.Reader) io.Reader { + return &deadlineResetReader{ + r: reader, + reset: c.resetReadDeadline, + + lastReset: time.Now(), + } +} + +type deadlineResetReader struct { + r io.Reader + reset func() + + lastReset time.Time +} + +func (r *deadlineResetReader) Read(p []byte) (n int, err error) { + n, err = r.r.Read(p) + if time.Since(r.lastReset) > onReadDeadlineResetInterval { + log.Warnw("slow/large read, resetting deadline while reading the frame", "since", time.Since(r.lastReset), "n", n, "err", err, "p", len(p)) + + r.reset() + r.lastReset = time.Now() + } + return +} + +func (c *wsConn) resetReadDeadline() { + if c.timeout > 0 { + if err := c.conn.SetReadDeadline(time.Now().Add(c.timeout)); err != nil { + log.Error("setting read deadline", err) + } + } +} + +// Takes an ID as received on the wire, validates it, and translates it to a +// normalized ID appropriate for keying. +func normalizeID(id interface{}) (interface{}, error) { + switch v := id.(type) { + case string, float64, nil: + return v, nil + case int64: // clients sending int64 need to normalize to float64 + return float64(v), nil + default: + return nil, xerrors.Errorf("invalid id type: %T", id) + } +} diff --git a/go.work b/go.work index 4fe57a047..352a91b08 100644 --- a/go.work +++ b/go.work @@ -8,6 +8,7 @@ use ( ./common/cool ./common/serialize/bitset ./common/serialize/bytearray + ./common/serialize/go-jsonrpc ./common/serialize/log ./common/serialize/sturc ./common/serialize/xml diff --git a/logic/main.go b/logic/main.go index fd750e02e..1e1c9a019 100644 --- a/logic/main.go +++ b/logic/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "os" _ "github.com/gogf/gf/contrib/nosql/redis/v2" @@ -10,17 +9,10 @@ import ( _ "blazing/contrib/drivers/pgsql" - "github.com/gogf/gf/v2/util/gconv" - - "blazing/common/socket" - "blazing/common/socket/handler" "blazing/cool" - "blazing/logic/controller" //"blazing/o/service" - "blazing/modules/blazing/service" - "github.com/gogf/gf/v2/os/gctx" ) @@ -35,6 +27,7 @@ func main() { } go Start(cool.Config.PortBL) //注入service + fmt.Println("Process start, pid:", os.Getpid()) gproc.AddSigHandlerShutdown( @@ -43,26 +36,3 @@ func main() { gproc.Listen() } - -// 如果id是0,那就是login server -func Start(serverid uint32) { - - head := handler.NewTomeeHandler() - head.Callback = controller.Recv - if serverid != 0 { - // 确定端口 - port, err := determinePort(serverid) - if err != nil { - log.Fatalf("Failed to determine port: %v", err) - } - - //随机端口产生,然后给sql注册端口 - service.NewLoginServiceService().SetServerID(serverid, gconv.Uint16(port)) - socket.NewServer(socket.WithCORS(), socket.WithPort(port), socket.WithSocketHandler(head)).Start() - - } else { - - socket.NewServer(socket.WithCORS(), socket.WithPort(defaultPort), socket.WithSocketHandler(head)).Start() - } - -} diff --git a/logic/rpc.go b/logic/rpc.go new file mode 100644 index 000000000..b0dc77f04 --- /dev/null +++ b/logic/rpc.go @@ -0,0 +1,123 @@ +package main + +import ( + "blazing/common/api" + "context" + "fmt" + "io" + "log" + "net" + "time" + + "google.golang.org/grpc" +) + +const rpcaddr = "127.0.0.1:9999" + +// Server +type Server struct { + api.UnimplementedBothWayStreamServerServer +} + +// DemoMethod 实现 proto 的方法 +func (s *Server) Call(server api.BothWayStreamServer_CallServer) error { + var count int + // 启一个携程监听读事件 + go func() { + for { + p, err := server.Recv() + if err != nil && err == io.EOF { + return + } + if err != nil { + fmt.Println("服务端接收错误", err) + break + } + fmt.Println("服务端接收数据", p.GetKickResponse().UserId) + } + }() + // 持续写事件 + for { + count++ + + err := server.Send(&api.GenericMessage{ + Payload: &api.GenericMessage_KickResponse{ + KickResponse: &api.KickRequest{ + UserId: int32(count), + }, + }, + }) + if err != nil { + break + } + time.Sleep(time.Second) + } + return nil +} + +func rpcserver() { + // 实例化一个 grpc 服务 + g := grpc.NewServer() + s := new(Server) + // 绑定 + api.RegisterBothWayStreamServerServer(g, s) + + // grpc 监听在 8888 端口 + l, err := net.Listen("tcp", rpcaddr) + if err != nil { + log.Fatal(err) + } + // 服务启动 + err = g.Serve(l) + if err != nil { + panic(err) + } +} + +func rpcclient() { + connect, err := grpc.Dial(rpcaddr, grpc.WithInsecure()) + if err != nil { + panic(err) + } + defer connect.Close() + client := api.NewBothWayStreamServerClient(connect) + stream, err := client.Call(context.Background()) + if err != nil { + panic(err) + } + // 接收数据 + go func() { + for { + reply, err := stream.Recv() + if err != nil { + break + } + fmt.Println("客户端接收到的数据是:", reply.Payload) + } + }() + + // 发送数据 + var count int + for { + count++ + err := stream.Send(&api.GenericMessage{ + Payload: &api.GenericMessage_KickResponse{ + KickResponse: &api.KickRequest{ + UserId: int32(count), + }, + }, + }) + if err != nil { + log.Printf("发送失败: %v", err) + break + } + time.Sleep(time.Second) + // // 10 次后发送关闭 + // if count == 10 { + // err := stream.CloseSend() + // if err != nil { + // break + // } + // } + } +} diff --git a/logic/server.go b/logic/server.go index fe988e0c9..2302e7fe7 100644 --- a/logic/server.go +++ b/logic/server.go @@ -1,7 +1,11 @@ package main import ( + "blazing/common/socket" + "blazing/common/socket/handler" "blazing/cool" + "blazing/logic/controller" + "blazing/modules/blazing/service" "fmt" "log" "math/rand" @@ -52,3 +56,28 @@ func isPortAvailable(port int) bool { defer listener.Close() return true } + +// 如果id是0,那就是login server +func Start(serverid uint32) { + //ants.NewPool(100) + head := handler.NewTomeeHandler() + head.Callback = controller.Recv + if serverid != 0 { //logic服务器 + // 确定端口 + port, err := determinePort(serverid) + if err != nil { + log.Fatalf("Failed to determine port: %v", err) + } + //go rpcclient() + //ants.Submit(rpcclient) + //随机端口产生,然后给sql注册端口 + service.NewLoginServiceService().SetServerID(serverid, gconv.Uint16(port)) + socket.NewServer(socket.WithCORS(), socket.WithPort(port), socket.WithSocketHandler(head)).Start() + + } else { + //go rpcserver() //对login tcp启动 + //ants.Submit(rpcserver) + socket.NewServer(socket.WithCORS(), socket.WithPort(defaultPort), socket.WithSocketHandler(head)).Start() + } + +} diff --git a/login/main.go b/login/main.go index bd9ddf180..522e490f6 100644 --- a/login/main.go +++ b/login/main.go @@ -26,5 +26,6 @@ import ( func main() { // go Start(cool.Config.Port) + //go rpc() cmd.Main.Run(gctx.New()) } diff --git a/manifest/proto/rpc.proto b/manifest/proto/rpc.proto new file mode 100644 index 000000000..7460d2680 --- /dev/null +++ b/manifest/proto/rpc.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +// 指定 Go 包路径(推荐使用完整的模块路径) +option go_package = "/common/api"; + +package api; + + +// 注册请求 - logic 用户登录后注册 +message RegisterUser { + int32 identity = 1; // 客户端身份,进入后保存id->端口 实现通知踢人以及进程退出 + int32 user_id = 2; // 执行踢人操作的用户id +} + +// 注册请求 - B客户端使用此消息向服务器注册 +message KickRequest { + int32 user_id = 1; // 执行踢人操作的用户id +} +// 函数描述符 +message FunctionDescriptor { + string function_name = 1; // 函数名称 + string input_type = 2; // 输入参数类型 + string output_type = 3; // 输出参数类型 + string description = 4; // 函数描述 +} + +// 注册响应 +message RegisterResponse { + bool success = 1; // 注册是否成功 + string message = 2; // 消息描述 + string registration_id = 3; // 注册ID +} + +// 函数调用请求 - A客户端使用此消息请求调用B的函数 +message FunctionCallRequest { + string target_client_id = 1; // 目标客户端ID(B) + string function_name = 2; // 要调用的函数名 + bytes parameters = 3; // 序列化后的函数参数 + string call_id = 4; // 调用ID,用于关联响应 +} + +// 函数调用响应 - 从B客户端返回给A客户端 +message FunctionCallResponse { + string call_id = 1; // 对应请求的调用ID + bool success = 2; // 调用是否成功 + bytes result = 3; // 序列化后的返回结果 + string error_message = 4; // 错误消息(如果失败) +} + +// 通用消息 +message GenericMessage { + oneof payload { + RegisterUser register_request = 1; + RegisterResponse register_response = 2; + FunctionCallRequest function_call_request = 3; + FunctionCallResponse function_call_response = 4; + string text_message = 5; // 普通文本消息 + KickRequest kick_response = 6; + + bool sucess = 7; + } +} + +// 主服务接口 +service BothWayStreamServer { + // 双向流连接 - 用于注册和函数调用 + rpc Call (stream GenericMessage) returns (stream GenericMessage); + + // 函数调用服务 - A客户端通过此方法请求调用B的函数 + //rpc CallFunction (FunctionCallRequest) returns (FunctionCallResponse); +} \ No newline at end of file diff --git a/modules/base/packed/packed.go b/modules/base/packed/packed.go index 50ff1c781..500fb006f 100644 --- a/modules/base/packed/packed.go +++ b/modules/base/packed/packed.go @@ -3,7 +3,7 @@ package packed import "github.com/gogf/gf/v2/os/gres" func init() { - if err := gres.Add("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"); err != nil { + if err := gres.Add("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"); err != nil { panic("add binary content to resource manager failed: " + err.Error()) } }