diff --git a/app/log/command/command.go b/app/log/command/command.go index 5672fb98c..9fd576233 100644 --- a/app/log/command/command.go +++ b/app/log/command/command.go @@ -4,14 +4,17 @@ package command import ( "context" + "time" grpc "google.golang.org/grpc" core "github.com/v2fly/v2ray-core/v4" "github.com/v2fly/v2ray-core/v4/app/log" "github.com/v2fly/v2ray-core/v4/common" + cmlog "github.com/v2fly/v2ray-core/v4/common/log" ) +// LoggerServer is the implemention of LoggerService type LoggerServer struct { V *core.Instance } @@ -31,6 +34,34 @@ func (s *LoggerServer) RestartLogger(ctx context.Context, request *RestartLogger return &RestartLoggerResponse{}, nil } +// FollowLog implements LoggerService. +func (s *LoggerServer) FollowLog(_ *FollowLogRequest, stream LoggerService_FollowLogServer) error { + logger := s.V.GetFeature((*log.Instance)(nil)) + if logger == nil { + return newError("unable to get logger instance") + } + follower, ok := logger.(cmlog.Follower) + if !ok { + return newError("logger not support following") + } + var err error + f := func(msg cmlog.Message) { + err = stream.Send(&FollowLogResponse{ + Message: msg.String(), + }) + } + follower.AddFollower(f) + defer follower.RemoveFollower(f) + ticker := time.NewTicker(time.Second) + for { + <-ticker.C + if err != nil { + ticker.Stop() + return nil + } + } +} + func (s *LoggerServer) mustEmbedUnimplementedLoggerServiceServer() {} type service struct { diff --git a/app/log/command/config.pb.go b/app/log/command/config.pb.go index c7988a3c6..36720d0f3 100644 --- a/app/log/command/config.pb.go +++ b/app/log/command/config.pb.go @@ -134,6 +134,91 @@ func (*RestartLoggerResponse) Descriptor() ([]byte, []int) { return file_app_log_command_config_proto_rawDescGZIP(), []int{2} } +type FollowLogRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *FollowLogRequest) Reset() { + *x = FollowLogRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_app_log_command_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FollowLogRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FollowLogRequest) ProtoMessage() {} + +func (x *FollowLogRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_log_command_config_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 FollowLogRequest.ProtoReflect.Descriptor instead. +func (*FollowLogRequest) Descriptor() ([]byte, []int) { + return file_app_log_command_config_proto_rawDescGZIP(), []int{3} +} + +type FollowLogResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *FollowLogResponse) Reset() { + *x = FollowLogResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_app_log_command_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FollowLogResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FollowLogResponse) ProtoMessage() {} + +func (x *FollowLogResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_log_command_config_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 FollowLogResponse.ProtoReflect.Descriptor instead. +func (*FollowLogResponse) Descriptor() ([]byte, []int) { + return file_app_log_command_config_proto_rawDescGZIP(), []int{4} +} + +func (x *FollowLogResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + var File_app_log_command_config_proto protoreflect.FileDescriptor var file_app_log_command_config_proto_rawDesc = []byte{ @@ -144,23 +229,34 @@ var file_app_log_command_config_proto_rawDesc = []byte{ 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x87, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x76, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, - 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, - 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x76, 0x32, 0x72, - 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, - 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, - 0x6f, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, - 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6c, 0x6f, 0x67, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1a, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, - 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, + 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x11, 0x46, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xf5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, + 0x67, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x76, 0x0a, 0x0d, 0x52, 0x65, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x12, 0x30, 0x2e, 0x76, 0x32, + 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, + 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, + 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x6c, 0x0a, 0x09, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x6f, 0x67, 0x12, + 0x2c, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x46, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, + 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, + 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, + 0x77, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, + 0x42, 0x6f, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, + 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6c, 0x6f, 0x67, 0x2f, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1a, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, + 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4c, 0x6f, 0x67, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -175,17 +271,21 @@ func file_app_log_command_config_proto_rawDescGZIP() []byte { return file_app_log_command_config_proto_rawDescData } -var file_app_log_command_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_app_log_command_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_app_log_command_config_proto_goTypes = []interface{}{ (*Config)(nil), // 0: v2ray.core.app.log.command.Config (*RestartLoggerRequest)(nil), // 1: v2ray.core.app.log.command.RestartLoggerRequest (*RestartLoggerResponse)(nil), // 2: v2ray.core.app.log.command.RestartLoggerResponse + (*FollowLogRequest)(nil), // 3: v2ray.core.app.log.command.FollowLogRequest + (*FollowLogResponse)(nil), // 4: v2ray.core.app.log.command.FollowLogResponse } var file_app_log_command_config_proto_depIdxs = []int32{ 1, // 0: v2ray.core.app.log.command.LoggerService.RestartLogger:input_type -> v2ray.core.app.log.command.RestartLoggerRequest - 2, // 1: v2ray.core.app.log.command.LoggerService.RestartLogger:output_type -> v2ray.core.app.log.command.RestartLoggerResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type + 3, // 1: v2ray.core.app.log.command.LoggerService.FollowLog:input_type -> v2ray.core.app.log.command.FollowLogRequest + 2, // 2: v2ray.core.app.log.command.LoggerService.RestartLogger:output_type -> v2ray.core.app.log.command.RestartLoggerResponse + 4, // 3: v2ray.core.app.log.command.LoggerService.FollowLog:output_type -> v2ray.core.app.log.command.FollowLogResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -233,6 +333,30 @@ func file_app_log_command_config_proto_init() { return nil } } + file_app_log_command_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FollowLogRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_app_log_command_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FollowLogResponse); 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{ @@ -240,7 +364,7 @@ func file_app_log_command_config_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_app_log_command_config_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/app/log/command/config.proto b/app/log/command/config.proto index a312ae695..7308e1571 100644 --- a/app/log/command/config.proto +++ b/app/log/command/config.proto @@ -12,6 +12,13 @@ message RestartLoggerRequest {} message RestartLoggerResponse {} +message FollowLogRequest {} + +message FollowLogResponse { + string message = 1; +} + service LoggerService { rpc RestartLogger(RestartLoggerRequest) returns (RestartLoggerResponse) {} + rpc FollowLog(FollowLogRequest) returns (stream FollowLogResponse) {}; } diff --git a/app/log/command/config_grpc.pb.go b/app/log/command/config_grpc.pb.go index 10a7d5313..9b51d111b 100644 --- a/app/log/command/config_grpc.pb.go +++ b/app/log/command/config_grpc.pb.go @@ -19,6 +19,7 @@ const _ = grpc.SupportPackageIsVersion7 // 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 LoggerServiceClient interface { RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error) + FollowLog(ctx context.Context, in *FollowLogRequest, opts ...grpc.CallOption) (LoggerService_FollowLogClient, error) } type loggerServiceClient struct { @@ -38,11 +39,44 @@ func (c *loggerServiceClient) RestartLogger(ctx context.Context, in *RestartLogg return out, nil } +func (c *loggerServiceClient) FollowLog(ctx context.Context, in *FollowLogRequest, opts ...grpc.CallOption) (LoggerService_FollowLogClient, error) { + stream, err := c.cc.NewStream(ctx, &LoggerService_ServiceDesc.Streams[0], "/v2ray.core.app.log.command.LoggerService/FollowLog", opts...) + if err != nil { + return nil, err + } + x := &loggerServiceFollowLogClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type LoggerService_FollowLogClient interface { + Recv() (*FollowLogResponse, error) + grpc.ClientStream +} + +type loggerServiceFollowLogClient struct { + grpc.ClientStream +} + +func (x *loggerServiceFollowLogClient) Recv() (*FollowLogResponse, error) { + m := new(FollowLogResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // LoggerServiceServer is the server API for LoggerService service. // All implementations must embed UnimplementedLoggerServiceServer // for forward compatibility type LoggerServiceServer interface { RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) + FollowLog(*FollowLogRequest, LoggerService_FollowLogServer) error mustEmbedUnimplementedLoggerServiceServer() } @@ -53,6 +87,9 @@ type UnimplementedLoggerServiceServer struct { func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RestartLogger not implemented") } +func (UnimplementedLoggerServiceServer) FollowLog(*FollowLogRequest, LoggerService_FollowLogServer) error { + return status.Errorf(codes.Unimplemented, "method FollowLog not implemented") +} func (UnimplementedLoggerServiceServer) mustEmbedUnimplementedLoggerServiceServer() {} // UnsafeLoggerServiceServer may be embedded to opt out of forward compatibility for this service. @@ -84,6 +121,27 @@ func _LoggerService_RestartLogger_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _LoggerService_FollowLog_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(FollowLogRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(LoggerServiceServer).FollowLog(m, &loggerServiceFollowLogServer{stream}) +} + +type LoggerService_FollowLogServer interface { + Send(*FollowLogResponse) error + grpc.ServerStream +} + +type loggerServiceFollowLogServer struct { + grpc.ServerStream +} + +func (x *loggerServiceFollowLogServer) Send(m *FollowLogResponse) error { + return x.ServerStream.SendMsg(m) +} + // LoggerService_ServiceDesc is the grpc.ServiceDesc for LoggerService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -96,6 +154,12 @@ var LoggerService_ServiceDesc = grpc.ServiceDesc{ Handler: _LoggerService_RestartLogger_Handler, }, }, - Streams: []grpc.StreamDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "FollowLog", + Handler: _LoggerService_FollowLog_Handler, + ServerStreams: true, + }, + }, Metadata: "app/log/command/config.proto", } diff --git a/app/log/log.go b/app/log/log.go index 256feb7fa..546965e14 100644 --- a/app/log/log.go +++ b/app/log/log.go @@ -4,6 +4,7 @@ package log import ( "context" + "reflect" "sync" "github.com/v2fly/v2ray-core/v4/common" @@ -16,6 +17,7 @@ type Instance struct { config *Config accessLogger log.Handler errorLogger log.Handler + followers map[reflect.Value]func(msg log.Message) active bool } @@ -89,6 +91,23 @@ func (g *Instance) Start() error { return g.startInternal() } +// AddFollower implements log.Follower. +func (g *Instance) AddFollower(f func(msg log.Message)) { + g.Lock() + defer g.Unlock() + if g.followers == nil { + g.followers = make(map[reflect.Value]func(msg log.Message)) + } + g.followers[reflect.ValueOf(f)] = f +} + +// RemoveFollower implements log.Follower. +func (g *Instance) RemoveFollower(f func(msg log.Message)) { + g.Lock() + defer g.Unlock() + delete(g.followers, reflect.ValueOf(f)) +} + // Handle implements log.Handler. func (g *Instance) Handle(msg log.Message) { g.RLock() @@ -98,6 +117,10 @@ func (g *Instance) Handle(msg log.Message) { return } + for _, f := range g.followers { + f(msg) + } + switch msg := msg.(type) { case *log.AccessMessage: if g.accessLogger != nil { diff --git a/app/router/balancing.go b/app/router/balancing.go index 8de382cf0..3b0ce653d 100644 --- a/app/router/balancing.go +++ b/app/router/balancing.go @@ -21,7 +21,7 @@ type Balancer struct { ohm outbound.Manager fallbackTag string - override overridden + override override } // PickOutbound picks the tag of a outbound diff --git a/app/router/balancing_override.go b/app/router/balancing_override.go index 0abc71bea..875fc10a7 100644 --- a/app/router/balancing_override.go +++ b/app/router/balancing_override.go @@ -43,39 +43,39 @@ func (r *Router) OverrideSelecting(balancer string, selects []string, validity t return nil } -type overriddenSettings struct { +type overrideSettings struct { selects []string until time.Time } -type overridden struct { +type override struct { access sync.RWMutex - settings overriddenSettings + settings overrideSettings } -// Get gets the overridden settings -func (o *overridden) Get() *overriddenSettings { +// Get gets the override settings +func (o *override) Get() *overrideSettings { o.access.RLock() defer o.access.RUnlock() if len(o.settings.selects) == 0 || time.Now().After(o.settings.until) { return nil } - return &overriddenSettings{ + return &overrideSettings{ selects: o.settings.selects, until: o.settings.until, } } -// Put updates the overridden settings -func (o *overridden) Put(selects []string, until time.Time) { +// Put updates the override settings +func (o *override) Put(selects []string, until time.Time) { o.access.Lock() defer o.access.Unlock() o.settings.selects = selects o.settings.until = until } -// Clear clears the overridden settings -func (o *overridden) Clear() { +// Clear clears the override settings +func (o *override) Clear() { o.access.Lock() defer o.access.Unlock() o.settings.selects = nil diff --git a/app/stats/command/command.go b/app/stats/command/command.go index fc922d91d..20ce33d7d 100644 --- a/app/stats/command/command.go +++ b/app/stats/command/command.go @@ -49,9 +49,20 @@ func (s *statsServer) GetStats(ctx context.Context, request *GetStatsRequest) (* } func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { - matcher, err := strmatcher.Substr.New(request.Pattern) - if err != nil { - return nil, err + mgroup := &strmatcher.MatcherGroup{} + if request.Pattern != "" { + request.Patterns = append(request.Patterns, request.Pattern) + } + t := strmatcher.Substr + if request.Regexp { + t = strmatcher.Regex + } + for _, p := range request.Patterns { + m, err := t.New(p) + if err != nil { + return nil, err + } + mgroup.Add(m) } response := &QueryStatsResponse{} @@ -62,7 +73,7 @@ func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest } manager.VisitCounters(func(name string, c feature_stats.Counter) bool { - if matcher.Match(name) { + if mgroup.Size() == 0 || len(mgroup.Match(name)) > 0 { var value int64 if request.Reset_ { value = c.Set(0) diff --git a/app/stats/command/command.pb.go b/app/stats/command/command.pb.go index b54840284..e3800a6a2 100644 --- a/app/stats/command/command.pb.go +++ b/app/stats/command/command.pb.go @@ -184,8 +184,11 @@ type QueryStatsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` - Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + // Deprecated, use Patterns instead + Pattern string `protobuf:"bytes,1,opt,name=pattern,proto3" json:"pattern,omitempty"` + Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"` + Patterns []string `protobuf:"bytes,3,rep,name=patterns,proto3" json:"patterns,omitempty"` + Regexp bool `protobuf:"varint,4,opt,name=regexp,proto3" json:"regexp,omitempty"` } func (x *QueryStatsRequest) Reset() { @@ -234,6 +237,20 @@ func (x *QueryStatsRequest) GetReset_() bool { return false } +func (x *QueryStatsRequest) GetPatterns() []string { + if x != nil { + return x.Patterns + } + return nil +} + +func (x *QueryStatsRequest) GetRegexp() bool { + if x != nil { + return x.Regexp + } + return false +} + type QueryStatsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -494,66 +511,70 @@ var file_app_stats_command_command_proto_rawDesc = []byte{ 0x73, 0x65, 0x12, 0x36, 0x0a, 0x04, 0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x43, 0x0a, 0x11, 0x51, 0x75, + 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x77, 0x0a, 0x11, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x73, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x22, - 0x4c, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x04, 0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, + 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x67, + 0x65, 0x78, 0x70, 0x22, 0x4c, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x04, 0x73, 0x74, 0x61, + 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, + 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xa2, 0x02, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, + 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, + 0x6d, 0x47, 0x43, 0x12, 0x14, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, + 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, + 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, + 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, + 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, + 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, + 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x32, 0xde, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x6b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, + 0x12, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, + 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2e, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, + 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x12, 0x71, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, + 0x2f, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, + 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x30, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, + 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x6e, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x12, 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, - 0x0f, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x22, 0xa2, 0x02, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, - 0x75, 0x74, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x4e, 0x75, 0x6d, - 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x75, 0x6d, - 0x47, 0x43, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x12, - 0x14, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, - 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, - 0x6c, 0x6f, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, - 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x03, 0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, - 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, - 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4c, 0x69, - 0x76, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, - 0x06, 0x55, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, - 0x70, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, - 0xde, 0x02, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x6b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2d, 0x2e, 0x76, - 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, - 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x76, 0x32, - 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x71, 0x0a, - 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2f, 0x2e, 0x76, 0x32, - 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, - 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x76, - 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, - 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x6e, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, - 0x2d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, - 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, - 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, - 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, - 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, - 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x42, 0x75, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, - 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, - 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1c, 0x56, 0x32, 0x52, 0x61, 0x79, - 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x75, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x72, + 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x34, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, + 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x1c, 0x56, + 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/app/stats/command/command.proto b/app/stats/command/command.proto index 3ebc518cb..13c6b01d3 100644 --- a/app/stats/command/command.proto +++ b/app/stats/command/command.proto @@ -23,8 +23,11 @@ message GetStatsResponse { } message QueryStatsRequest { + // Deprecated, use Patterns instead string pattern = 1; bool reset = 2; + repeated string patterns = 3; + bool regexp = 4; } message QueryStatsResponse { diff --git a/common/cmdarg/arg.go b/common/cmdarg/arg.go index 79b625058..1f6850e7d 100644 --- a/common/cmdarg/arg.go +++ b/common/cmdarg/arg.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "net/http" "net/url" - "os" "strings" "time" @@ -28,8 +27,6 @@ func LoadArgToBytes(arg string) (out []byte, err error) { switch { case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): out, err = FetchHTTPContent(arg) - case (arg == "stdin:"): - out, err = ioutil.ReadAll(os.Stdin) default: out, err = ioutil.ReadFile(arg) } diff --git a/common/log/log.go b/common/log/log.go index effc55863..ca59532af 100644 --- a/common/log/log.go +++ b/common/log/log.go @@ -16,6 +16,12 @@ type Handler interface { Handle(msg Message) } +// Follower is the interface for following logs. +type Follower interface { + AddFollower(func(msg Message)) + RemoveFollower(func(msg Message)) +} + // GeneralMessage is a general log message that can contain all kind of content. type GeneralMessage struct { Severity Severity diff --git a/common/units/bytesize.go b/common/units/bytesize.go new file mode 100644 index 000000000..be0a2b60f --- /dev/null +++ b/common/units/bytesize.go @@ -0,0 +1,98 @@ +package units + +import ( + "errors" + "strconv" + "strings" + "unicode" +) + +var errInvalidSize = errors.New("invalid size") +var errInvalidUnit = errors.New("invalid or unsupported unit") + +// ByteSize is the size of bytes +type ByteSize uint64 + +const ( + _ = iota + // KB = 1KB + KB ByteSize = 1 << (10 * iota) + // MB = 1MB + MB + // GB = 1GB + GB + // TB = 1TB + TB + // PB = 1PB + PB + // EB = 1EB + EB +) + +func (b ByteSize) String() string { + unit := "" + value := float64(0) + switch { + case b == 0: + return "0" + case b < KB: + unit = "B" + value = float64(b) + case b < MB: + unit = "KB" + value = float64(b) / float64(KB) + case b < GB: + unit = "MB" + value = float64(b) / float64(MB) + case b < TB: + unit = "GB" + value = float64(b) / float64(GB) + case b < PB: + unit = "TB" + value = float64(b) / float64(TB) + case b < EB: + unit = "PB" + value = float64(b) / float64(PB) + default: + unit = "EB" + value = float64(b) / float64(EB) + } + result := strconv.FormatFloat(value, 'f', 2, 64) + result = strings.TrimSuffix(result, ".0") + return result + unit +} + +// Parse parses ByteSize from string +func (b *ByteSize) Parse(s string) error { + s = strings.TrimSpace(s) + s = strings.ToUpper(s) + i := strings.IndexFunc(s, unicode.IsLetter) + if i == -1 { + return errInvalidUnit + } + + bytesString, multiple := s[:i], s[i:] + bytes, err := strconv.ParseFloat(bytesString, 64) + if err != nil || bytes <= 0 { + return errInvalidSize + } + switch multiple { + case "B": + *b = ByteSize(bytes) + case "K", "KB", "KIB": + *b = ByteSize(bytes * float64(KB)) + case "M", "MB", "MIB": + *b = ByteSize(bytes * float64(MB)) + case "G", "GB", "GIB": + *b = ByteSize(bytes * float64(GB)) + case "T", "TB", "TIB": + *b = ByteSize(bytes * float64(TB)) + case "P", "PB", "PIB": + *b = ByteSize(bytes * float64(PB)) + case "E", "EB", "EIB": + *b = ByteSize(bytes * float64(EB)) + default: + return errInvalidUnit + } + return nil +} diff --git a/common/units/bytesize_test.go b/common/units/bytesize_test.go new file mode 100644 index 000000000..8bcb307e9 --- /dev/null +++ b/common/units/bytesize_test.go @@ -0,0 +1,66 @@ +package units_test + +import ( + "testing" + + "github.com/v2fly/v2ray-core/v4/common/units" +) + +func TestByteSizes(t *testing.T) { + size := units.ByteSize(0) + assertSizeString(t, size, "0") + size++ + assertSizeValue(t, + assertSizeString(t, size, "1.00B"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00KB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00MB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00GB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00TB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00PB"), + size, + ) + size <<= 10 + assertSizeValue(t, + assertSizeString(t, size, "1.00EB"), + size, + ) +} + +func assertSizeValue(t *testing.T, size string, expected units.ByteSize) { + actual := units.ByteSize(0) + err := actual.Parse(size) + if err != nil { + t.Error(err) + } + if actual != expected { + t.Errorf("expect %s, but got %s", expected, actual) + } +} + +func assertSizeString(t *testing.T, size units.ByteSize, expected string) string { + actual := size.String() + if actual != expected { + t.Errorf("expect %s, but got %s", expected, actual) + } + return expected +} diff --git a/config.go b/config.go index 5aa825944..475b428bf 100644 --- a/config.go +++ b/config.go @@ -1,8 +1,12 @@ package core import ( + "fmt" "io" + "log" + "os" "path/filepath" + "reflect" "strings" "google.golang.org/protobuf/proto" @@ -12,6 +16,21 @@ import ( "github.com/v2fly/v2ray-core/v4/common/cmdarg" ) +const ( + // FormatAuto represents all available formats by auto selecting + FormatAuto = "auto" + // FormatJSON represents json format + FormatJSON = "json" + // FormatTOML represents toml format + FormatTOML = "toml" + // FormatYAML represents yaml format + FormatYAML = "yaml" + // FormatProtobuf represents protobuf format + FormatProtobuf = "protobuf" + // FormatProtobufShort is the short of FormatProtobuf + FormatProtobufShort = "pb" +) + // ConfigFormat is a configurable format of V2Ray config file. type ConfigFormat struct { Name []string @@ -23,6 +42,7 @@ type ConfigFormat struct { type ConfigLoader func(input interface{}) (*Config, error) var ( + configLoaders = make([]*ConfigFormat, 0) configLoaderByName = make(map[string]*ConfigFormat) configLoaderByExt = make(map[string]*ConfigFormat) ) @@ -30,11 +50,10 @@ var ( // RegisterConfigLoader add a new ConfigLoader. func RegisterConfigLoader(format *ConfigFormat) error { for _, name := range format.Name { - lname := strings.ToLower(name) - if _, found := configLoaderByName[lname]; found { + if _, found := configLoaderByName[name]; found { return newError(name, " already registered.") } - configLoaderByName[lname] = format + configLoaderByName[name] = format } for _, ext := range format.Extension { @@ -44,7 +63,7 @@ func RegisterConfigLoader(format *ConfigFormat) error { } configLoaderByExt[lext] = format } - + configLoaders = append(configLoaders, format) return nil } @@ -53,43 +72,101 @@ func getExtension(filename string) string { return strings.ToLower(ext) } -// GetConfigLoader get config loader by name and filename. -// Specify formatName to explicitly select a loader. -// Specify filename to choose loader by detect its extension. -// Leave formatName and filename blank for default loader -func GetConfigLoader(formatName string, filename string) (*ConfigFormat, error) { - if formatName != "" { - // if explicitly specified, we can safely assume that user knows what they are - if f, found := configLoaderByName[formatName]; found { - return f, nil - } - return nil, newError("Unable to load config in ", formatName).AtWarning() +// GetLoaderExtensions get config loader extensions. +func GetLoaderExtensions(formatName string) ([]string, error) { + if formatName == FormatAuto { + return GetAllExtensions(), nil } - // no explicitly specified loader, extenstion detect first - if ext := getExtension(filename); len(ext) > 0 { - if f, found := configLoaderByExt[ext]; found { - return f, nil - } + if f, found := configLoaderByName[formatName]; found { + return f.Extension, nil } - // default loader - if f, found := configLoaderByName["json"]; found { - return f, nil - } - panic("default loader not found") + return nil, newError("config loader not found: ", formatName).AtWarning() } -// LoadConfig loads config with given format from given source. -// input accepts 2 different types: +// GetAllExtensions get all extensions supported +func GetAllExtensions() []string { + extensions := make([]string, 0) + for _, f := range configLoaderByName { + extensions = append(extensions, f.Extension...) + } + return extensions +} + +// LoadConfig loads multiple config with given format from given source. +// input accepts: +// * string of a single filename/url(s) to open to read // * []string slice of multiple filename/url(s) to open to read // * io.Reader that reads a config content (the original way) -func LoadConfig(formatName string, filename string, input interface{}) (*Config, error) { - f, err := GetConfigLoader(formatName, filename) - if err != nil { - return nil, err +func LoadConfig(formatName string, input interface{}) (*Config, error) { + cnt := getInputCount(input) + if cnt == 0 { + log.Println("Using config from STDIN") + input = os.Stdin + cnt = 1 + } + if formatName == FormatAuto && cnt == 1 { + // This ensures only to call auto loader for multiple files, + // so that it can only care about merging scenarios + return loadSingleConfigAutoFormat(input) + } + // if input is a slice with single element, extract it + // so that unmergeable loaders don't need to deal with + // slices + s := reflect.Indirect(reflect.ValueOf(input)) + k := s.Kind() + if (k == reflect.Slice || k == reflect.Array) && s.Len() == 1 { + value := reflect.Indirect(s.Index(0)) + if value.Kind() == reflect.String { + // string type alias + input = fmt.Sprint(value.Interface()) + } else { + input = value.Interface() + } + } + f, found := configLoaderByName[formatName] + if !found { + return nil, newError("config loader not found: ", formatName).AtWarning() } return f.Loader(input) } +// loadSingleConfigAutoFormat loads a single config with from given source. +// input accepts: +// * string of a single filename/url(s) to open to read +// * io.Reader that reads a config content (the original way) +func loadSingleConfigAutoFormat(input interface{}) (*Config, error) { + if file, ok := input.(string); ok { + extension := getExtension(file) + if extension != "" { + lowerName := strings.ToLower(extension) + if f, found := configLoaderByExt[lowerName]; found { + return f.Loader(file) + } + return nil, newError("config loader not found for: ", extension).AtWarning() + } + } + // no extension, try all loaders + for _, f := range configLoaders { + if f.Name[0] == FormatAuto { + continue + } + c, err := f.Loader(input) + if err == nil { + return c, nil + } + } + return nil, newError("tried all loaders but failed for: ", input).AtWarning() +} + +func getInputCount(input interface{}) int { + s := reflect.Indirect(reflect.ValueOf(input)) + k := s.Kind() + if k == reflect.Slice || k == reflect.Array { + return s.Len() + } + return 1 +} + func loadProtobufConfig(data []byte) (*Config, error) { config := new(Config) if err := proto.Unmarshal(data, config); err != nil { @@ -100,12 +177,12 @@ func loadProtobufConfig(data []byte) (*Config, error) { func init() { common.Must(RegisterConfigLoader(&ConfigFormat{ - Name: []string{"Protobuf", "pb"}, + Name: []string{FormatProtobuf, FormatProtobufShort}, Extension: []string{".pb"}, Loader: func(input interface{}) (*Config, error) { switch v := input.(type) { - case cmdarg.Arg: - r, err := cmdarg.LoadArg(v[0]) + case string: + r, err := cmdarg.LoadArg(v) if err != nil { return nil, err } diff --git a/features/routing/health.go b/features/routing/health.go index d061c0f76..5959be309 100644 --- a/features/routing/health.go +++ b/features/routing/health.go @@ -26,7 +26,7 @@ type StrategyInfo struct { Others []*OutboundInfo // Other outbounds } -// BalancingOverrideInfo holds balancing overridden information +// BalancingOverrideInfo holds balancing override information type BalancingOverrideInfo struct { Until time.Time Selects []string diff --git a/functions.go b/functions.go index 88deeaa50..559b04176 100644 --- a/functions.go +++ b/functions.go @@ -24,7 +24,7 @@ func CreateObject(v *Instance, config interface{}) (interface{}, error) { // // v2ray:api:stable func StartInstance(configFormat string, configBytes []byte) (*Instance, error) { - config, err := LoadConfig(configFormat, "", bytes.NewReader(configBytes)) + config, err := LoadConfig(configFormat, bytes.NewReader(configBytes)) if err != nil { return nil, err } diff --git a/functions_test.go b/functions_test.go index 7a3d3476d..51e60bbf7 100644 --- a/functions_test.go +++ b/functions_test.go @@ -61,7 +61,7 @@ func TestV2RayDial(t *testing.T) { cfgBytes, err := proto.Marshal(config) common.Must(err) - server, err := core.StartInstance("protobuf", cfgBytes) + server, err := core.StartInstance(core.FormatProtobuf, cfgBytes) common.Must(err) defer server.Close() @@ -111,7 +111,7 @@ func TestV2RayDialUDPConn(t *testing.T) { cfgBytes, err := proto.Marshal(config) common.Must(err) - server, err := core.StartInstance("protobuf", cfgBytes) + server, err := core.StartInstance(core.FormatProtobuf, cfgBytes) common.Must(err) defer server.Close() @@ -178,7 +178,7 @@ func TestV2RayDialUDP(t *testing.T) { cfgBytes, err := proto.Marshal(config) common.Must(err) - server, err := core.StartInstance("protobuf", cfgBytes) + server, err := core.StartInstance(core.FormatProtobuf, cfgBytes) common.Must(err) defer server.Close() diff --git a/infra/conf/merge/map.go b/infra/conf/merge/map.go index eb3098f24..452d09bac 100644 --- a/infra/conf/merge/map.go +++ b/infra/conf/merge/map.go @@ -9,6 +9,7 @@ import ( ) // mergeMaps merges source map into target +// it supports only map[string]interface{} type for any children of the map tree func mergeMaps(target map[string]interface{}, source map[string]interface{}) (err error) { for key, value := range source { target[key], err = mergeField(target[key], value) diff --git a/infra/conf/merge/merge.go b/infra/conf/merge/merge.go index 305af8ba8..0d2cccd00 100644 --- a/infra/conf/merge/merge.go +++ b/infra/conf/merge/merge.go @@ -18,95 +18,46 @@ package merge import ( "bytes" "encoding/json" - "fmt" - "io" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" "github.com/v2fly/v2ray-core/v4/infra/conf/serial" ) -// FilesToJSON merges multiple jsons files into one json, accepts remote url, or local file path -func FilesToJSON(args []string) ([]byte, error) { - m, err := FilesToMap(args) - if err != nil { - return nil, err - } - return json.Marshal(m) -} - -// BytesToJSON merges multiple json contents into one json. -func BytesToJSON(args [][]byte) ([]byte, error) { - m, err := BytesToMap(args) - if err != nil { - return nil, err - } - return json.Marshal(m) -} - -// FilesToMap merges multiple json files into one map, accepts remote url, or local file path -func FilesToMap(args []string) (m map[string]interface{}, err error) { - m, err = loadFiles(args) - if err != nil { - return nil, err - } - err = applyRules(m) - if err != nil { - return nil, err - } - return m, nil -} - -// BytesToMap merges multiple json contents into one map. -func BytesToMap(args [][]byte) (m map[string]interface{}, err error) { - m, err = loadBytes(args) - if err != nil { - return nil, err - } - err = applyRules(m) - if err != nil { - return nil, err - } - return m, nil -} - -func loadFiles(args []string) (map[string]interface{}, error) { - c := make(map[string]interface{}) +// JSONs merges multiple json contents into one json. +func JSONs(args [][]byte) ([]byte, error) { + m := make(map[string]interface{}) for _, arg := range args { - r, err := cmdarg.LoadArg(arg) - if err != nil { - return nil, fmt.Errorf("fail to load %s: %s", arg, err) - } - m, err := decode(r) - if err != nil { - return nil, fmt.Errorf("fail to decode %s: %s", arg, err) - } - if err = mergeMaps(c, m); err != nil { - return nil, fmt.Errorf("fail to merge %s: %s", arg, err) - } - } - return c, nil -} - -func loadBytes(args [][]byte) (map[string]interface{}, error) { - conf := make(map[string]interface{}) - for _, arg := range args { - r := bytes.NewReader(arg) - m, err := decode(r) - if err != nil { - return nil, err - } - if err = mergeMaps(conf, m); err != nil { + if _, err := ToMap(arg, m); err != nil { return nil, err } } - return conf, nil + return FromMap(m) } -func decode(r io.Reader) (map[string]interface{}, error) { - c := make(map[string]interface{}) - err := serial.DecodeJSON(r, &c) +// ToMap merges json content to target map and returns it +func ToMap(content []byte, target map[string]interface{}) (map[string]interface{}, error) { + if target == nil { + target = make(map[string]interface{}) + } + r := bytes.NewReader(content) + n := make(map[string]interface{}) + err := serial.DecodeJSON(r, &n) if err != nil { return nil, err } - return c, nil + if err = mergeMaps(target, n); err != nil { + return nil, err + } + return target, nil +} + +// FromMap apply merge rules to map and convert it to json +func FromMap(target map[string]interface{}) ([]byte, error) { + if target == nil { + target = make(map[string]interface{}) + } + err := ApplyRules(target) + if err != nil { + return nil, err + } + return json.Marshal(target) } diff --git a/infra/conf/merge/merge_test.go b/infra/conf/merge/merge_test.go index b8d56d7c2..5c1f6e9f3 100644 --- a/infra/conf/merge/merge_test.go +++ b/infra/conf/merge/merge_test.go @@ -5,7 +5,7 @@ package merge_test import ( - "encoding/json" + "bytes" "reflect" "strings" "testing" @@ -50,7 +50,7 @@ func TestMergeV2Style(t *testing.T) { ]} } ` - m, err := merge.BytesToMap([][]byte{[]byte(json1), []byte(json2)}) + m, err := merge.JSONs([][]byte{[]byte(json1), []byte(json2)}) if err != nil { t.Error(err) } @@ -91,7 +91,7 @@ func TestMergeTag(t *testing.T) { } } ` - m, err := merge.BytesToMap([][]byte{[]byte(json1), []byte(json2)}) + m, err := merge.JSONs([][]byte{[]byte(json1), []byte(json2)}) if err != nil { t.Error(err) } @@ -147,7 +147,7 @@ func TestMergeTagValueTypes(t *testing.T) { }] } ` - m, err := merge.BytesToMap([][]byte{[]byte(json1), []byte(json2)}) + m, err := merge.JSONs([][]byte{[]byte(json1), []byte(json2)}) if err != nil { t.Error(err) } @@ -187,20 +187,24 @@ func TestMergeTagDeep(t *testing.T) { }] } ` - m, err := merge.BytesToMap([][]byte{[]byte(json1), []byte(json2)}) + m, err := merge.JSONs([][]byte{[]byte(json1), []byte(json2)}) if err != nil { t.Error(err) } assertResult(t, m, expected) } -func assertResult(t *testing.T, value map[string]interface{}, expected string) { - e := make(map[string]interface{}) - err := serial.DecodeJSON(strings.NewReader(expected), &e) +func assertResult(t *testing.T, value []byte, expected string) { + v := make(map[string]interface{}) + err := serial.DecodeJSON(bytes.NewReader(value), &v) if err != nil { t.Error(err) } - if !reflect.DeepEqual(value, e) { - bs, _ := json.Marshal(value) - t.Fatalf("expected:\n%s\n\nactual:\n%s", expected, string(bs)) + e := make(map[string]interface{}) + err = serial.DecodeJSON(strings.NewReader(expected), &e) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(v, e) { + t.Fatalf("expected:\n%s\n\nactual:\n%s", expected, string(value)) } } diff --git a/infra/conf/merge/rules.go b/infra/conf/merge/rules.go index a1150cea6..4ab51d922 100644 --- a/infra/conf/merge/rules.go +++ b/infra/conf/merge/rules.go @@ -7,7 +7,8 @@ package merge const priorityKey string = "_priority" const tagKey string = "_tag" -func applyRules(m map[string]interface{}) error { +// ApplyRules applies merge rules according to _tag, _priority fields, and remove them +func ApplyRules(m map[string]interface{}) error { err := sortMergeSlices(m) if err != nil { return err diff --git a/main/json/errors.generated.go b/infra/conf/mergers/errors.generated.go similarity index 92% rename from main/json/errors.generated.go rename to infra/conf/mergers/errors.generated.go index eb6c6c5c1..877b2ed7e 100644 --- a/main/json/errors.generated.go +++ b/infra/conf/mergers/errors.generated.go @@ -1,4 +1,4 @@ -package json +package mergers import "github.com/v2fly/v2ray-core/v4/common/errors" diff --git a/infra/conf/mergers/extensions.go b/infra/conf/mergers/extensions.go new file mode 100644 index 000000000..d627b1edb --- /dev/null +++ b/infra/conf/mergers/extensions.go @@ -0,0 +1,25 @@ +package mergers + +import "strings" + +// GetExtensions get extensions of given format +func GetExtensions(formatName string) ([]string, error) { + lowerName := strings.ToLower(formatName) + if lowerName == "auto" { + return GetAllExtensions(), nil + } + f, found := mergeLoaderByName[lowerName] + if !found { + return nil, newError(formatName+" not found", formatName).AtWarning() + } + return f.Extensions, nil +} + +// GetAllExtensions get all extensions supported +func GetAllExtensions() []string { + extensions := make([]string, 0) + for _, f := range mergeLoaderByName { + extensions = append(extensions, f.Extensions...) + } + return extensions +} diff --git a/infra/conf/mergers/formats.go b/infra/conf/mergers/formats.go new file mode 100644 index 000000000..c2f1350a5 --- /dev/null +++ b/infra/conf/mergers/formats.go @@ -0,0 +1,40 @@ +package mergers + +//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen + +import ( + "strings" +) + +// MergeableFormat is a configurable mergeable format of V2Ray config file. +type MergeableFormat struct { + Name string + Extensions []string + Loader MergeLoader +} + +// MergeLoader is a utility to merge V2Ray config from external source into a map and returns it. +type MergeLoader func(input interface{}, m map[string]interface{}) error + +var ( + mergeLoaderByName = make(map[string]*MergeableFormat) + mergeLoaderByExt = make(map[string]*MergeableFormat) +) + +// RegisterMergeLoader add a new MergeLoader. +func RegisterMergeLoader(format *MergeableFormat) error { + if _, found := mergeLoaderByName[format.Name]; found { + return newError(format.Name, " already registered.") + } + mergeLoaderByName[format.Name] = format + + for _, ext := range format.Extensions { + lext := strings.ToLower(ext) + if f, found := mergeLoaderByExt[lext]; found { + return newError(ext, " already registered to ", f.Name) + } + mergeLoaderByExt[lext] = format + } + + return nil +} diff --git a/infra/conf/mergers/merge.go b/infra/conf/mergers/merge.go new file mode 100644 index 000000000..9b151d633 --- /dev/null +++ b/infra/conf/mergers/merge.go @@ -0,0 +1,95 @@ +package mergers + +import ( + "io" + "io/ioutil" + "path/filepath" + "strings" + + core "github.com/v2fly/v2ray-core/v4" + "github.com/v2fly/v2ray-core/v4/common/cmdarg" +) + +// MergeAs load input and merge as specified format into m +func MergeAs(formatName string, input interface{}, m map[string]interface{}) error { + f, found := mergeLoaderByName[formatName] + if !found { + return newError("format loader not found for: ", formatName) + } + return f.Loader(input, m) +} + +// Merge loads inputs and merges them into m +// it detects extension for loader selecting, or try all loaders +// if no extension found +func Merge(input interface{}, m map[string]interface{}) error { + switch v := input.(type) { + case string: + err := mergeSingleFile(v, m) + if err != nil { + return err + } + case []string: + for _, file := range v { + err := mergeSingleFile(file, m) + if err != nil { + return err + } + } + case cmdarg.Arg: + for _, file := range v { + err := mergeSingleFile(file, m) + if err != nil { + return err + } + } + case []byte: + err := mergeSingleFile(v, m) + if err != nil { + return err + } + case io.Reader: + // read to []byte incase it tries different loaders + bs, err := ioutil.ReadAll(v) + if err != nil { + return err + } + err = mergeSingleFile(bs, m) + if err != nil { + return err + } + default: + return newError("unknow merge input type") + } + return nil +} + +func mergeSingleFile(input interface{}, m map[string]interface{}) error { + if file, ok := input.(string); ok { + ext := getExtension(file) + if ext != "" { + lext := strings.ToLower(ext) + f, found := mergeLoaderByExt[lext] + if !found { + return newError("unmergeable format extension: ", ext) + } + return f.Loader(file, m) + } + } + // no extension, try all loaders + for _, f := range mergeLoaderByName { + if f.Name == core.FormatAuto { + continue + } + err := f.Loader(input, m) + if err == nil { + return nil + } + } + return newError("tried all loaders but failed for: ", input).AtWarning() +} + +func getExtension(filename string) string { + ext := filepath.Ext(filename) + return strings.ToLower(ext) +} diff --git a/infra/conf/mergers/merger_base.go b/infra/conf/mergers/merger_base.go new file mode 100644 index 000000000..cddeb556b --- /dev/null +++ b/infra/conf/mergers/merger_base.go @@ -0,0 +1,103 @@ +package mergers + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/v2fly/v2ray-core/v4/common/cmdarg" + "github.com/v2fly/v2ray-core/v4/infra/conf/merge" +) + +type jsonConverter func(v []byte) ([]byte, error) + +func makeLoader(name string, extensions []string, converter jsonConverter) *MergeableFormat { + return &MergeableFormat{ + Name: name, + Extensions: extensions, + Loader: makeConvertToJSONLoader(converter), + } +} + +func makeConvertToJSONLoader(converter func(v []byte) ([]byte, error)) MergeLoader { + return func(input interface{}, target map[string]interface{}) error { + if target == nil { + panic("merge target is nil") + } + switch v := input.(type) { + case string: + err := loadFile(v, target, converter) + if err != nil { + return err + } + case []string: + err := loadFiles(v, target, converter) + if err != nil { + return err + } + case cmdarg.Arg: + err := loadFiles(v, target, converter) + if err != nil { + return err + } + case []byte: + err := loadBytes(v, target, converter) + if err != nil { + return err + } + case io.Reader: + err := loadReader(v, target, converter) + if err != nil { + return err + } + default: + return newError("unknow merge input type") + } + return nil + } +} + +func loadFiles(files []string, target map[string]interface{}, converter func(v []byte) ([]byte, error)) error { + for _, file := range files { + err := loadFile(file, target, converter) + if err != nil { + return err + } + } + return nil +} + +func loadFile(file string, target map[string]interface{}, converter func(v []byte) ([]byte, error)) error { + bs, err := cmdarg.LoadArgToBytes(file) + if err != nil { + return fmt.Errorf("fail to load %s: %s", file, err) + } + if converter != nil { + bs, err = converter(bs) + if err != nil { + return fmt.Errorf("error convert to json '%s': %s", file, err) + } + } + _, err = merge.ToMap(bs, target) + return err +} + +func loadReader(reader io.Reader, target map[string]interface{}, converter func(v []byte) ([]byte, error)) error { + bs, err := ioutil.ReadAll(reader) + if err != nil { + return err + } + return loadBytes(bs, target, converter) +} + +func loadBytes(bs []byte, target map[string]interface{}, converter func(v []byte) ([]byte, error)) error { + var err error + if converter != nil { + bs, err = converter(bs) + if err != nil { + return fmt.Errorf("fail to convert to json: %s", err) + } + } + _, err = merge.ToMap(bs, target) + return err +} diff --git a/infra/conf/mergers/mergers.go b/infra/conf/mergers/mergers.go new file mode 100644 index 000000000..5f9ad5144 --- /dev/null +++ b/infra/conf/mergers/mergers.go @@ -0,0 +1,32 @@ +package mergers + +import ( + core "github.com/v2fly/v2ray-core/v4" + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/infra/conf/json" +) + +func init() { + common.Must(RegisterMergeLoader(makeLoader( + core.FormatJSON, + []string{".json", ".jsonc"}, + nil, + ))) + common.Must(RegisterMergeLoader(makeLoader( + core.FormatTOML, + []string{".toml"}, + json.FromTOML, + ))) + common.Must(RegisterMergeLoader(makeLoader( + core.FormatYAML, + []string{".yml", ".yaml"}, + json.FromYAML, + ))) + common.Must(RegisterMergeLoader( + &MergeableFormat{ + Name: core.FormatAuto, + Extensions: nil, + Loader: Merge, + }), + ) +} diff --git a/infra/conf/mergers/names.go b/infra/conf/mergers/names.go new file mode 100644 index 000000000..adbfa9e0f --- /dev/null +++ b/infra/conf/mergers/names.go @@ -0,0 +1,10 @@ +package mergers + +// GetAllNames get names of all formats +func GetAllNames() []string { + names := make([]string, 0) + for _, f := range mergeLoaderByName { + names = append(names, f.Name) + } + return names +} diff --git a/main/commands/all/api/api.go b/main/commands/all/api/api.go index 5915d3479..1e7493bfe 100644 --- a/main/commands/all/api/api.go +++ b/main/commands/all/api/api.go @@ -7,14 +7,12 @@ import ( // CmdAPI calls an API in an V2Ray process var CmdAPI = &base.Command{ UsageLine: "{{.Exec}} api", - Short: "Call V2Ray API", + Short: "call V2Ray API", Long: `{{.Exec}} {{.LongName}} provides tools to manipulate V2Ray via its API. `, Commands: []*base.Command{ - cmdRestartLogger, - cmdGetStats, - cmdQueryStats, - cmdSysStats, + cmdLog, + cmdStats, cmdBalancerCheck, cmdBalancerInfo, cmdBalancerOverride, diff --git a/main/commands/all/api/balancer_check.go b/main/commands/all/api/balancer_check.go index 7bf171f57..9dd3d9855 100644 --- a/main/commands/all/api/balancer_check.go +++ b/main/commands/all/api/balancer_check.go @@ -18,10 +18,10 @@ of server config. Arguments: - -s, -server + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: diff --git a/main/commands/all/api/balancer_info.go b/main/commands/all/api/balancer_info.go index afdf36d9c..3a18d3191 100644 --- a/main/commands/all/api/balancer_info.go +++ b/main/commands/all/api/balancer_info.go @@ -10,6 +10,7 @@ import ( "github.com/v2fly/v2ray-core/v4/main/commands/base" ) +// TODO: support "-json" flag for json output var cmdBalancerInfo = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...", @@ -24,10 +25,13 @@ of server config. Arguments: - -s, -server + -json + Use json output. + + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -53,12 +57,17 @@ func executeBalancerInfo(cmd *base.Command, args []string) { sort.Slice(resp.Balancers, func(i, j int) bool { return resp.Balancers[i].Tag < resp.Balancers[j].Tag }) + if apiJSON { + showJSONResponse(resp) + return + } for _, b := range resp.Balancers { showBalancerInfo(b) } } func showBalancerInfo(b *routerService.BalancerMsg) { + const tableIndent = 4 sb := new(strings.Builder) // Balancer sb.WriteString(fmt.Sprintf("Balancer: %s\n", b.Tag)) @@ -71,25 +80,28 @@ func showBalancerInfo(b *routerService.BalancerMsg) { if b.Override != nil { sb.WriteString(" - Selecting Override:\n") until := fmt.Sprintf("until: %s", b.Override.Until) - writeRow(sb, 0, nil, nil, until) + writeRow(sb, tableIndent, 0, []string{until}, nil) for i, s := range b.Override.Selects { - writeRow(sb, i+1, nil, nil, s) + writeRow(sb, tableIndent, i+1, []string{s}, nil) } } + b.Titles = append(b.Titles, "Tag") formats := getColumnFormats(b.Titles) // Selects sb.WriteString(" - Selects:\n") - writeRow(sb, 0, b.Titles, formats, "Tag") + writeRow(sb, tableIndent, 0, b.Titles, formats) for i, o := range b.Selects { - writeRow(sb, i+1, o.Values, formats, o.Tag) + o.Values = append(o.Values, o.Tag) + writeRow(sb, tableIndent, i+1, o.Values, formats) } // Others scnt := len(b.Selects) if len(b.Others) > 0 { sb.WriteString(" - Others:\n") - writeRow(sb, 0, b.Titles, formats, "Tag") + writeRow(sb, tableIndent, 0, b.Titles, formats) for i, o := range b.Others { - writeRow(sb, scnt+i+1, o.Values, formats, o.Tag) + o.Values = append(o.Values, o.Tag) + writeRow(sb, tableIndent, scnt+i+1, o.Values, formats) } } os.Stdout.WriteString(sb.String()) @@ -103,12 +115,12 @@ func getColumnFormats(titles []string) []string { return w } -func writeRow(sb *strings.Builder, index int, values, formats []string, tag string) { +func writeRow(sb *strings.Builder, indent, index int, values, formats []string) { if index == 0 { // title line - sb.WriteString(" ") + sb.WriteString(strings.Repeat(" ", indent+4)) } else { - sb.WriteString(fmt.Sprintf(" %-4d", index)) + sb.WriteString(fmt.Sprintf("%s%-4d", strings.Repeat(" ", indent), index)) } for i, v := range values { format := "%-14s" @@ -117,6 +129,5 @@ func writeRow(sb *strings.Builder, index int, values, formats []string, tag stri } sb.WriteString(fmt.Sprintf(format, v)) } - sb.WriteString(tag) sb.WriteByte('\n') } diff --git a/main/commands/all/api/balancer_override.go b/main/commands/all/api/balancer_override.go index f675792b3..0a4ac40ea 100644 --- a/main/commands/all/api/balancer_override.go +++ b/main/commands/all/api/balancer_override.go @@ -10,14 +10,14 @@ import ( var cmdBalancerOverride = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> selectors...", - Short: "balancer select override", + Short: "balancer override", Long: ` -Override a balancer's selecting in a duration of time. +Override a balancer's selection in a duration of time. > Make sure you have "RoutingService" set in "config.api.services" of server config. -Once a balancer's selecting is overridden: +Once a balancer's selection is overridden: - The selectors of the balancer won't apply. - The strategy of the balancer stops selecting qualified nodes @@ -25,19 +25,20 @@ Once a balancer's selecting is overridden: Arguments: + -b, -balancer + Tag of the target balancer. Required. + + -v, -validity + Time duration of the validity of override, e.g.: 60s, 60m, + 24h, 1m30s. Default 1h. + -r, -remove - Remove the overridden + Remove the override - -b, -balancer - Tag of the balancer. Required - - -v, -validity - Time minutes of the validity of overridden. Default 60 - - -s, -server + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -51,13 +52,13 @@ Example: func executeBalancerOverride(cmd *base.Command, args []string) { var ( balancer string - validity int64 + validity time.Duration remove bool ) cmd.Flag.StringVar(&balancer, "b", "", "") cmd.Flag.StringVar(&balancer, "balancer", "", "") - cmd.Flag.Int64Var(&validity, "v", 60, "") - cmd.Flag.Int64Var(&validity, "validity", 60, "") + cmd.Flag.DurationVar(&validity, "v", time.Hour, "") + cmd.Flag.DurationVar(&validity, "validity", time.Hour, "") cmd.Flag.BoolVar(&remove, "r", false, "") cmd.Flag.BoolVar(&remove, "remove", false, "") setSharedFlags(cmd) @@ -72,7 +73,7 @@ func executeBalancerOverride(cmd *base.Command, args []string) { v := int64(0) if !remove { - v = int64(time.Duration(validity) * time.Minute) + v = int64(validity) } client := routerService.NewRoutingServiceClient(conn) r := &routerService.OverrideSelectingRequest{ @@ -82,6 +83,6 @@ func executeBalancerOverride(cmd *base.Command, args []string) { } _, err := client.OverrideSelecting(ctx, r) if err != nil { - base.Fatalf("failed to perform balancer health checks: %s", err) + base.Fatalf("failed to override balancer: %s", err) } } diff --git a/main/commands/all/api/inbounds_add.go b/main/commands/all/api/inbounds_add.go index 6284260ab..82f0cc73f 100644 --- a/main/commands/all/api/inbounds_add.go +++ b/main/commands/all/api/inbounds_add.go @@ -4,25 +4,31 @@ import ( "fmt" handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" "github.com/v2fly/v2ray-core/v4/main/commands/base" + "github.com/v2fly/v2ray-core/v4/main/commands/helpers" ) var cmdAddInbounds = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api adi [--server=127.0.0.1:8080] [c2.json]...", - Short: "Add inbounds", + Short: "add inbounds", Long: ` Add inbounds to V2Ray. Arguments: - -s, -server + -format + Specify the input format. + Available values: "auto", "json", "toml", "yaml" + Default: "auto" + + -r + Load folders recursively. + + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -34,26 +40,13 @@ Example: func executeAddInbounds(cmd *base.Command, args []string) { setSharedFlags(cmd) + setSharedConfigFlags(cmd) cmd.Flag.Parse(args) - unnamedArgs := cmd.Flag.Args() - if len(unnamedArgs) == 0 { - fmt.Println("reading from stdin:") - unnamedArgs = []string{"stdin:"} + c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively) + if err != nil { + base.Fatalf("%s", err) } - - ins := make([]conf.InboundDetourConfig, 0) - for _, arg := range unnamedArgs { - r, err := cmdarg.LoadArg(arg) - if err != nil { - base.Fatalf("failed to load %s: %s", arg, err) - } - conf, err := serial.DecodeJSONConfig(r) - if err != nil { - base.Fatalf("failed to decode %s: %s", arg, err) - } - ins = append(ins, conf.InboundConfigs...) - } - if len(ins) == 0 { + if len(c.InboundConfigs) == 0 { base.Fatalf("no valid inbound found") } @@ -61,7 +54,7 @@ func executeAddInbounds(cmd *base.Command, args []string) { defer close() client := handlerService.NewHandlerServiceClient(conn) - for _, in := range ins { + for _, in := range c.InboundConfigs { fmt.Println("adding:", in.Tag) i, err := in.Build() if err != nil { @@ -70,10 +63,9 @@ func executeAddInbounds(cmd *base.Command, args []string) { r := &handlerService.AddInboundRequest{ Inbound: i, } - resp, err := client.AddInbound(ctx, r) + _, err = client.AddInbound(ctx, r) if err != nil { base.Fatalf("failed to add inbound: %s", err) } - showResponese(resp) } } diff --git a/main/commands/all/api/inbounds_remove.go b/main/commands/all/api/inbounds_remove.go index 9fe08d51f..5259e0522 100644 --- a/main/commands/all/api/inbounds_remove.go +++ b/main/commands/all/api/inbounds_remove.go @@ -4,24 +4,31 @@ import ( "fmt" handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" "github.com/v2fly/v2ray-core/v4/main/commands/base" + "github.com/v2fly/v2ray-core/v4/main/commands/helpers" ) var cmdRemoveInbounds = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api rmi [--server=127.0.0.1:8080] [json_file] [tag]...", - Short: "Remove inbounds", + Short: "remove inbounds", Long: ` Remove inbounds from V2Ray. Arguments: - -s, -server + -format + Specify the input format. + Available values: "auto", "json", "toml", "yaml" + Default: "auto" + + -r + Load folders recursively. + + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -33,48 +40,28 @@ Example: func executeRemoveInbounds(cmd *base.Command, args []string) { setSharedFlags(cmd) + setSharedConfigFlags(cmd) cmd.Flag.Parse(args) - unnamedArgs := cmd.Flag.Args() - if len(unnamedArgs) == 0 { - fmt.Println("reading from stdin:") - unnamedArgs = []string{"stdin:"} + c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively) + if err != nil { + base.Fatalf("%s", err) } - - tags := make([]string, 0) - for _, arg := range unnamedArgs { - if r, err := cmdarg.LoadArg(arg); err == nil { - conf, err := serial.DecodeJSONConfig(r) - if err != nil { - base.Fatalf("failed to decode %s: %s", arg, err) - } - ins := conf.InboundConfigs - for _, i := range ins { - tags = append(tags, i.Tag) - } - } else { - // take request as tag - tags = append(tags, arg) - } - } - - if len(tags) == 0 { + if len(c.InboundConfigs) == 0 { base.Fatalf("no inbound to remove") } - fmt.Println("removing inbounds:", tags) conn, ctx, close := dialAPIServer() defer close() client := handlerService.NewHandlerServiceClient(conn) - for _, tag := range tags { - fmt.Println("removing:", tag) + for _, c := range c.InboundConfigs { + fmt.Println("removing:", c.Tag) r := &handlerService.RemoveInboundRequest{ - Tag: tag, + Tag: c.Tag, } - resp, err := client.RemoveInbound(ctx, r) + _, err := client.RemoveInbound(ctx, r) if err != nil { base.Fatalf("failed to remove inbound: %s", err) } - showResponese(resp) } } diff --git a/main/commands/all/api/log.go b/main/commands/all/api/log.go new file mode 100644 index 000000000..34a9ce41e --- /dev/null +++ b/main/commands/all/api/log.go @@ -0,0 +1,83 @@ +package api + +import ( + "io" + "log" + + logService "github.com/v2fly/v2ray-core/v4/app/log/command" + "github.com/v2fly/v2ray-core/v4/main/commands/base" +) + +var cmdLog = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api log [--server=127.0.0.1:8080]", + Short: "log operations", + Long: ` +Follow and print logs from v2ray. + +> It ignores -timeout flag while following logs + +Arguments: + + -restart + Restart the logger + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} + {{.Exec}} {{.LongName}} --restart +`, + Run: executeRestartLogger, +} + +func executeRestartLogger(cmd *base.Command, args []string) { + var restart bool + cmd.Flag.BoolVar(&restart, "restart", false, "") + setSharedFlags(cmd) + cmd.Flag.Parse(args) + + if restart { + restartLogger() + return + } + followLogger() +} + +func restartLogger() { + conn, ctx, close := dialAPIServer() + defer close() + client := logService.NewLoggerServiceClient(conn) + r := &logService.RestartLoggerRequest{} + _, err := client.RestartLogger(ctx, r) + if err != nil { + base.Fatalf("failed to restart logger: %s", err) + } +} + +func followLogger() { + conn, ctx, close := dialAPIServerWithoutTimeout() + defer close() + client := logService.NewLoggerServiceClient(conn) + r := &logService.FollowLogRequest{} + stream, err := client.FollowLog(ctx, r) + if err != nil { + base.Fatalf("failed to follow logger: %s", err) + } + + for { + resp, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + base.Fatalf("failed to fetch log: %s", err) + } + log.Print(resp.Message) + } +} diff --git a/main/commands/all/api/logger_restart.go b/main/commands/all/api/logger_restart.go deleted file mode 100644 index 6a97af095..000000000 --- a/main/commands/all/api/logger_restart.go +++ /dev/null @@ -1,40 +0,0 @@ -package api - -import ( - logService "github.com/v2fly/v2ray-core/v4/app/log/command" - "github.com/v2fly/v2ray-core/v4/main/commands/base" -) - -var cmdRestartLogger = &base.Command{ - CustomFlags: true, - UsageLine: "{{.Exec}} api restartlogger [--server=127.0.0.1:8080]", - Short: "Restart the logger", - Long: ` -Restart the logger of V2Ray. - -Arguments: - - -s, -server - The API server address. Default 127.0.0.1:8080 - - -t, -timeout - Timeout seconds to call API. Default 3 -`, - Run: executeRestartLogger, -} - -func executeRestartLogger(cmd *base.Command, args []string) { - setSharedFlags(cmd) - cmd.Flag.Parse(args) - - conn, ctx, close := dialAPIServer() - defer close() - - client := logService.NewLoggerServiceClient(conn) - r := &logService.RestartLoggerRequest{} - resp, err := client.RestartLogger(ctx, r) - if err != nil { - base.Fatalf("failed to restart logger: %s", err) - } - showResponese(resp) -} diff --git a/main/commands/all/api/outbounds_add.go b/main/commands/all/api/outbounds_add.go index 17da414ca..15db1ffa4 100644 --- a/main/commands/all/api/outbounds_add.go +++ b/main/commands/all/api/outbounds_add.go @@ -4,25 +4,31 @@ import ( "fmt" handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" "github.com/v2fly/v2ray-core/v4/main/commands/base" + "github.com/v2fly/v2ray-core/v4/main/commands/helpers" ) var cmdAddOutbounds = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api ado [--server=127.0.0.1:8080] [c2.json]...", - Short: "Add outbounds", + Short: "add outbounds", Long: ` Add outbounds to V2Ray. Arguments: - -s, -server + -format + Specify the input format. + Available values: "auto", "json", "toml", "yaml" + Default: "auto" + + -r + Load folders recursively. + + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -34,26 +40,13 @@ Example: func executeAddOutbounds(cmd *base.Command, args []string) { setSharedFlags(cmd) + setSharedConfigFlags(cmd) cmd.Flag.Parse(args) - unnamedArgs := cmd.Flag.Args() - if len(unnamedArgs) == 0 { - fmt.Println("Reading from STDIN") - unnamedArgs = []string{"stdin:"} + c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively) + if err != nil { + base.Fatalf("%s", err) } - - outs := make([]conf.OutboundDetourConfig, 0) - for _, arg := range unnamedArgs { - r, err := cmdarg.LoadArg(arg) - if err != nil { - base.Fatalf("failed to load %s: %s", arg, err) - } - conf, err := serial.DecodeJSONConfig(r) - if err != nil { - base.Fatalf("failed to decode %s: %s", arg, err) - } - outs = append(outs, conf.OutboundConfigs...) - } - if len(outs) == 0 { + if len(c.OutboundConfigs) == 0 { base.Fatalf("no valid outbound found") } @@ -61,7 +54,7 @@ func executeAddOutbounds(cmd *base.Command, args []string) { defer close() client := handlerService.NewHandlerServiceClient(conn) - for _, out := range outs { + for _, out := range c.OutboundConfigs { fmt.Println("adding:", out.Tag) o, err := out.Build() if err != nil { @@ -70,10 +63,9 @@ func executeAddOutbounds(cmd *base.Command, args []string) { r := &handlerService.AddOutboundRequest{ Outbound: o, } - resp, err := client.AddOutbound(ctx, r) + _, err = client.AddOutbound(ctx, r) if err != nil { base.Fatalf("failed to add outbound: %s", err) } - showResponese(resp) } } diff --git a/main/commands/all/api/outbounds_remove.go b/main/commands/all/api/outbounds_remove.go index b95d73787..2f97b5ab0 100644 --- a/main/commands/all/api/outbounds_remove.go +++ b/main/commands/all/api/outbounds_remove.go @@ -4,24 +4,31 @@ import ( "fmt" handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" "github.com/v2fly/v2ray-core/v4/main/commands/base" + "github.com/v2fly/v2ray-core/v4/main/commands/helpers" ) var cmdRemoveOutbounds = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} api rmo [--server=127.0.0.1:8080] [json_file] [tag]...", - Short: "Remove outbounds", + Short: "remove outbounds", Long: ` Remove outbounds from V2Ray. Arguments: - -s, -server + -format + Specify the input format. + Available values: "auto", "json", "toml", "yaml" + Default: "auto" + + -r + Load folders recursively. + + -s, -server The API server address. Default 127.0.0.1:8080 - -t, -timeout + -t, -timeout Timeout seconds to call API. Default 3 Example: @@ -34,30 +41,12 @@ Example: func executeRemoveOutbounds(cmd *base.Command, args []string) { setSharedFlags(cmd) cmd.Flag.Parse(args) - unnamedArgs := cmd.Flag.Args() - if len(unnamedArgs) == 0 { - fmt.Println("reading from stdin:") - unnamedArgs = []string{"stdin:"} + setSharedConfigFlags(cmd) + c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively) + if err != nil { + base.Fatalf("%s", err) } - - tags := make([]string, 0) - for _, arg := range unnamedArgs { - if r, err := cmdarg.LoadArg(arg); err == nil { - conf, err := serial.DecodeJSONConfig(r) - if err != nil { - base.Fatalf("failed to decode %s: %s", arg, err) - } - outs := conf.OutboundConfigs - for _, o := range outs { - tags = append(tags, o.Tag) - } - } else { - // take request as tag - tags = append(tags, arg) - } - } - - if len(tags) == 0 { + if len(c.OutboundConfigs) == 0 { base.Fatalf("no outbound to remove") } @@ -65,15 +54,14 @@ func executeRemoveOutbounds(cmd *base.Command, args []string) { defer close() client := handlerService.NewHandlerServiceClient(conn) - for _, tag := range tags { - fmt.Println("removing:", tag) + for _, c := range c.OutboundConfigs { + fmt.Println("removing:", c.Tag) r := &handlerService.RemoveOutboundRequest{ - Tag: tag, + Tag: c.Tag, } - resp, err := client.RemoveOutbound(ctx, r) + _, err := client.RemoveOutbound(ctx, r) if err != nil { base.Fatalf("failed to remove outbound: %s", err) } - showResponese(resp) } } diff --git a/main/commands/all/api/shared.go b/main/commands/all/api/shared.go index 8a9b16573..0bd6913ad 100644 --- a/main/commands/all/api/shared.go +++ b/main/commands/all/api/shared.go @@ -5,18 +5,22 @@ import ( "encoding/json" "fmt" "os" - "reflect" "strings" "time" "github.com/v2fly/v2ray-core/v4/main/commands/base" "google.golang.org/grpc" "google.golang.org/protobuf/proto" + + core "github.com/v2fly/v2ray-core/v4" ) var ( - apiServerAddrPtr string - apiTimeout int + apiServerAddrPtr string + apiTimeout int + apiJSON bool + apiConfigFormat string + apiConfigRecursively bool ) func setSharedFlags(cmd *base.Command) { @@ -24,14 +28,17 @@ func setSharedFlags(cmd *base.Command) { cmd.Flag.StringVar(&apiServerAddrPtr, "server", "127.0.0.1:8080", "") cmd.Flag.IntVar(&apiTimeout, "t", 3, "") cmd.Flag.IntVar(&apiTimeout, "timeout", 3, "") + cmd.Flag.BoolVar(&apiJSON, "json", false, "") +} + +func setSharedConfigFlags(cmd *base.Command) { + cmd.Flag.StringVar(&apiConfigFormat, "format", core.FormatAuto, "") + cmd.Flag.BoolVar(&apiConfigRecursively, "r", false, "") } func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(apiTimeout)*time.Second) - conn, err := grpc.DialContext(ctx, apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock()) - if err != nil { - base.Fatalf("failed to dial %s", apiServerAddrPtr) - } + conn = dialAPIServerWithContext(ctx) close = func() { cancel() conn.Close() @@ -39,56 +46,40 @@ func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) return } -func showResponese(m proto.Message) { - if isEmpty(m) { - // avoid outputs like `{}`, `{"key":{}}` - return +func dialAPIServerWithoutTimeout() (conn *grpc.ClientConn, ctx context.Context, close func()) { + ctx = context.Background() + conn = dialAPIServerWithContext(ctx) + close = func() { + conn.Close() } + return +} + +func dialAPIServerWithContext(ctx context.Context) (conn *grpc.ClientConn) { + conn, err := grpc.DialContext(ctx, apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + base.Fatalf("failed to dial %s", apiServerAddrPtr) + } + return +} + +func protoToJSONString(m proto.Message, prefix, indent string) (string, error) { b := new(strings.Builder) e := json.NewEncoder(b) - e.SetIndent("", " ") + e.SetIndent(prefix, indent) e.SetEscapeHTML(false) err := e.Encode(m) if err != nil { - fmt.Fprintf(os.Stdout, "%v\n", m) - base.Fatalf("error encode json: %s", err) - return + return "", err } - fmt.Println(strings.TrimSpace(b.String())) + return strings.TrimSpace(b.String()), nil } -// isEmpty checks if the response is empty (all zero values). -// proto.Message types always "omitempty" on fields, -// there's no chance for a response to show zero-value messages, -// so we can perform isZero test here -func isEmpty(response interface{}) bool { - s := reflect.Indirect(reflect.ValueOf(response)) - if s.Kind() == reflect.Invalid { - return true +func showJSONResponse(m proto.Message) { + output, err := protoToJSONString(m, "", "") + if err != nil { + fmt.Fprintf(os.Stdout, "%v\n", m) + base.Fatalf("error encode json: %s", err) } - switch s.Kind() { - case reflect.Struct: - for i := 0; i < s.NumField(); i++ { - f := s.Type().Field(i) - if f.Name[0] < 65 || f.Name[0] > 90 { - // continue if not exported. - continue - } - field := s.Field(i) - if !isEmpty(field.Interface()) { - return false - } - } - case reflect.Array, reflect.Slice: - for i := 0; i < s.Len(); i++ { - if !isEmpty(s.Index(i).Interface()) { - return false - } - } - default: - if !s.IsZero() { - return false - } - } - return true + fmt.Println(output) } diff --git a/main/commands/all/api/shared_test.go b/main/commands/all/api/shared_test.go deleted file mode 100644 index ab428a6b9..000000000 --- a/main/commands/all/api/shared_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package api - -import ( - "testing" - - statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" -) - -func TestEmptyResponese_0(t *testing.T) { - r := &statsService.QueryStatsResponse{ - Stat: []*statsService.Stat{ - { - Name: "1>>2", - Value: 1, - }, - { - Name: "1>>2>>3", - Value: 2, - }, - }, - } - assert(t, isEmpty(r), false) -} - -func TestEmptyResponese_1(t *testing.T) { - r := (*statsService.QueryStatsResponse)(nil) - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_2(t *testing.T) { - r := &statsService.QueryStatsResponse{ - Stat: nil, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_3(t *testing.T) { - r := &statsService.QueryStatsResponse{ - Stat: []*statsService.Stat{}, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_4(t *testing.T) { - r := &statsService.QueryStatsResponse{ - Stat: []*statsService.Stat{ - { - Name: "", - Value: 0, - }, - }, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_5(t *testing.T) { - type test struct { - Value *statsService.QueryStatsResponse - } - r := &test{ - Value: &statsService.QueryStatsResponse{ - Stat: []*statsService.Stat{ - { - Name: "", - }, - }, - }, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_6(t *testing.T) { - type test struct { - Value *statsService.QueryStatsResponse - } - r := &test{ - Value: &statsService.QueryStatsResponse{ - Stat: []*statsService.Stat{ - { - Value: 1, - }, - }, - }, - } - assert(t, isEmpty(r), false) -} - -func TestEmptyResponese_7(t *testing.T) { - type test struct { - Value *int - } - v := 1 - r := &test{ - Value: &v, - } - assert(t, isEmpty(r), false) -} - -func TestEmptyResponese_8(t *testing.T) { - type test struct { - Value *int - } - v := 0 - r := &test{ - Value: &v, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_9(t *testing.T) { - assert(t, isEmpty(0), true) -} - -func TestEmptyResponese_10(t *testing.T) { - assert(t, isEmpty(1), false) -} - -func TestEmptyResponese_11(t *testing.T) { - r := []*statsService.Stat{ - { - Name: "", - }, - } - assert(t, isEmpty(r), true) -} - -func TestEmptyResponese_12(t *testing.T) { - r := []*statsService.Stat{ - { - Value: 1, - }, - } - assert(t, isEmpty(r), false) -} - -func assert(t *testing.T, value, expected bool) { - if value != expected { - t.Fatalf("Expected: %v, actual: %v", expected, value) - } -} diff --git a/main/commands/all/api/stats.go b/main/commands/all/api/stats.go new file mode 100644 index 000000000..8f9ba327d --- /dev/null +++ b/main/commands/all/api/stats.go @@ -0,0 +1,165 @@ +package api + +import ( + "fmt" + "os" + "sort" + "strings" + "time" + + statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" + "github.com/v2fly/v2ray-core/v4/common/units" + "github.com/v2fly/v2ray-core/v4/main/commands/base" +) + +var cmdStats = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api stats [--server=127.0.0.1:8080] [pattern]...", + Short: "query statistics", + Long: ` +Query statistics from V2Ray. + +Arguments: + + -regexp + The patterns are using regexp. + + -reset + Fetch values then reset statistics counters to 0. + + -runtime + Get runtime statistics. + + -json + Use json output. + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} -runtime + {{.Exec}} {{.LongName}} node1 + {{.Exec}} {{.LongName}} -json node1 node2 + {{.Exec}} {{.LongName}} -regexp 'node1.+downlink' +`, + Run: executeStats, +} + +func executeStats(cmd *base.Command, args []string) { + setSharedFlags(cmd) + var ( + runtime bool + regexp bool + reset bool + ) + cmd.Flag.BoolVar(&runtime, "runtime", false, "") + cmd.Flag.BoolVar(®exp, "regexp", false, "") + cmd.Flag.BoolVar(&reset, "reset", false, "") + cmd.Flag.Parse(args) + unnamed := cmd.Flag.Args() + if runtime { + getRuntimeStats(apiJSON) + return + } + getStats(unnamed, regexp, reset, apiJSON) +} + +func getRuntimeStats(jsonOutput bool) { + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.SysStatsRequest{} + resp, err := client.GetSysStats(ctx, r) + if err != nil { + base.Fatalf("failed to get sys stats: %s", err) + } + if jsonOutput { + showJSONResponse(resp) + return + } + showRuntimeStats(resp) +} + +func showRuntimeStats(s *statsService.SysStatsResponse) { + formats := []string{"%-22s", "%-10s"} + rows := [][]string{ + {"Up time", (time.Duration(s.Uptime) * time.Second).String()}, + {"Memory obtained", units.ByteSize(s.Sys).String()}, + {"Number of goroutines", fmt.Sprintf("%d", s.NumGoroutine)}, + {"Heap allocated", units.ByteSize(s.Alloc).String()}, + {"Live objects", fmt.Sprintf("%d", s.LiveObjects)}, + {"Heap allocated total", units.ByteSize(s.TotalAlloc).String()}, + {"Heap allocate count", fmt.Sprintf("%d", s.Mallocs)}, + {"Heap free count", fmt.Sprintf("%d", s.Frees)}, + {"Number of GC", fmt.Sprintf("%d", s.NumGC)}, + {"Time of GC pause", (time.Duration(s.PauseTotalNs) * time.Nanosecond).String()}, + } + sb := new(strings.Builder) + writeRow(sb, 0, 0, + []string{"Item", "Value"}, + formats, + ) + for i, r := range rows { + writeRow(sb, 0, i+1, r, formats) + } + os.Stdout.WriteString(sb.String()) +} + +func getStats(patterns []string, regexp, reset, jsonOutput bool) { + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.QueryStatsRequest{ + Patterns: patterns, + Regexp: regexp, + Reset_: reset, + } + resp, err := client.QueryStats(ctx, r) + if err != nil { + base.Fatalf("failed to query stats: %s", err) + } + if jsonOutput { + showJSONResponse(resp) + return + } + sort.Slice(resp.Stat, func(i, j int) bool { + return resp.Stat[i].Name < resp.Stat[j].Name + }) + showStats(resp.Stat) +} + +func showStats(stats []*statsService.Stat) { + if len(stats) == 0 { + return + } + formats := []string{"%-12s", "%s"} + sum := int64(0) + sb := new(strings.Builder) + idx := 0 + writeRow(sb, 0, 0, + []string{"Value", "Name"}, + formats, + ) + for _, stat := range stats { + // if stat.Value == 0 { + // continue + // } + idx++ + sum += stat.Value + writeRow( + sb, 0, idx, + []string{units.ByteSize(stat.Value).String(), stat.Name}, + formats, + ) + } + sb.WriteString( + fmt.Sprintf("\nTotal: %s\n", units.ByteSize(sum)), + ) + os.Stdout.WriteString(sb.String()) +} diff --git a/main/commands/all/api/stats_get.go b/main/commands/all/api/stats_get.go deleted file mode 100644 index 48aef9edd..000000000 --- a/main/commands/all/api/stats_get.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -import ( - statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" - "github.com/v2fly/v2ray-core/v4/main/commands/base" -) - -var cmdGetStats = &base.Command{ - CustomFlags: true, - UsageLine: "{{.Exec}} api stats [--server=127.0.0.1:8080] [-name '']", - Short: "Get statistics", - Long: ` -Get statistics from V2Ray. - -Arguments: - - -s, -server - The API server address. Default 127.0.0.1:8080 - - -t, -timeout - Timeout seconds to call API. Default 3 - - -name - Name of the stat counter. - - -reset - Reset the counter to fetching its value. - -Example: - - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -name "inbound>>>statin>>>traffic>>>downlink" -`, - Run: executeGetStats, -} - -func executeGetStats(cmd *base.Command, args []string) { - setSharedFlags(cmd) - statName := cmd.Flag.String("name", "", "") - reset := cmd.Flag.Bool("reset", false, "") - cmd.Flag.Parse(args) - - conn, ctx, close := dialAPIServer() - defer close() - - client := statsService.NewStatsServiceClient(conn) - r := &statsService.GetStatsRequest{ - Name: *statName, - Reset_: *reset, - } - resp, err := client.GetStats(ctx, r) - if err != nil { - base.Fatalf("failed to get stats: %s", err) - } - showResponese(resp) -} diff --git a/main/commands/all/api/stats_query.go b/main/commands/all/api/stats_query.go deleted file mode 100644 index 6ee9912c7..000000000 --- a/main/commands/all/api/stats_query.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -import ( - statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" - "github.com/v2fly/v2ray-core/v4/main/commands/base" -) - -var cmdQueryStats = &base.Command{ - CustomFlags: true, - UsageLine: "{{.Exec}} api statsquery [--server=127.0.0.1:8080] [-pattern '']", - Short: "Query statistics", - Long: ` -Query statistics from V2Ray. - -Arguments: - - -s, -server - The API server address. Default 127.0.0.1:8080 - - -t, -timeout - Timeout seconds to call API. Default 3 - - -pattern - Pattern of the query. - - -reset - Reset the counter to fetching its value. - -Example: - - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -pattern "counter_" -`, - Run: executeQueryStats, -} - -func executeQueryStats(cmd *base.Command, args []string) { - setSharedFlags(cmd) - pattern := cmd.Flag.String("pattern", "", "") - reset := cmd.Flag.Bool("reset", false, "") - cmd.Flag.Parse(args) - - conn, ctx, close := dialAPIServer() - defer close() - - client := statsService.NewStatsServiceClient(conn) - r := &statsService.QueryStatsRequest{ - Pattern: *pattern, - Reset_: *reset, - } - resp, err := client.QueryStats(ctx, r) - if err != nil { - base.Fatalf("failed to query stats: %s", err) - } - showResponese(resp) -} diff --git a/main/commands/all/api/stats_sys.go b/main/commands/all/api/stats_sys.go deleted file mode 100644 index 39f328dc2..000000000 --- a/main/commands/all/api/stats_sys.go +++ /dev/null @@ -1,40 +0,0 @@ -package api - -import ( - statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" - "github.com/v2fly/v2ray-core/v4/main/commands/base" -) - -var cmdSysStats = &base.Command{ - CustomFlags: true, - UsageLine: "{{.Exec}} api statssys [--server=127.0.0.1:8080]", - Short: "Get system statistics", - Long: ` -Get system statistics from V2Ray. - -Arguments: - - -s, -server - The API server address. Default 127.0.0.1:8080 - - -t, -timeout - Timeout seconds to call API. Default 3 -`, - Run: executeSysStats, -} - -func executeSysStats(cmd *base.Command, args []string) { - setSharedFlags(cmd) - cmd.Flag.Parse(args) - - conn, ctx, close := dialAPIServer() - defer close() - - client := statsService.NewStatsServiceClient(conn) - r := &statsService.SysStatsRequest{} - resp, err := client.GetSysStats(ctx, r) - if err != nil { - base.Fatalf("failed to get sys stats: %s", err) - } - showResponese(resp) -} diff --git a/main/commands/all/convert.go b/main/commands/all/convert.go index 115470d8b..d6199dc41 100644 --- a/main/commands/all/convert.go +++ b/main/commands/all/convert.go @@ -3,38 +3,42 @@ package all import ( "bytes" "encoding/json" - "github.com/pelletier/go-toml" - "google.golang.org/protobuf/proto" "os" "strings" + "github.com/pelletier/go-toml" + "google.golang.org/protobuf/proto" + + core "github.com/v2fly/v2ray-core/v4" + "github.com/v2fly/v2ray-core/v4/infra/conf/merge" "github.com/v2fly/v2ray-core/v4/infra/conf/serial" "github.com/v2fly/v2ray-core/v4/main/commands/base" + "github.com/v2fly/v2ray-core/v4/main/commands/helpers" "gopkg.in/yaml.v2" ) var cmdConvert = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} convert [c1.json] [.json] [dir1] ...", - Short: "Convert config files", + Short: "convert config files", Long: ` Convert config files between different formats. Files are merged before convert if multiple assigned. Arguments: - -i, -input + -i, -input Specify the input format. - Available values: "json", "toml", "yaml" - Default: "json" + Available values: "auto", "json", "toml", "yaml" + Default: "auto" - -o, -output + -o, -output Specify the output format Available values: "json", "toml", "yaml", "protobuf" / "pb" Default: "json" -r - Load confdir recursively. + Load folders recursively. Examples: @@ -61,15 +65,10 @@ var ( outputFormat string confDirRecursively bool ) -var formatExtensions = map[string][]string{ - "json": {".json", ".jsonc"}, - "toml": {".toml"}, - "yaml": {".yaml", ".yml"}, -} func setConfArgs(cmd *base.Command) { - cmd.Flag.StringVar(&inputFormat, "input", "json", "") - cmd.Flag.StringVar(&inputFormat, "i", "json", "") + cmd.Flag.StringVar(&inputFormat, "input", core.FormatAuto, "") + cmd.Flag.StringVar(&inputFormat, "i", core.FormatAuto, "") cmd.Flag.StringVar(&outputFormat, "output", "json", "") cmd.Flag.StringVar(&outputFormat, "o", "json", "") cmd.Flag.BoolVar(&confDirRecursively, "r", false, "") @@ -77,37 +76,36 @@ func setConfArgs(cmd *base.Command) { func executeConvert(cmd *base.Command, args []string) { setConfArgs(cmd) cmd.Flag.Parse(args) - unnamed := cmd.Flag.Args() inputFormat = strings.ToLower(inputFormat) outputFormat = strings.ToLower(outputFormat) - files := resolveFolderToFiles(unnamed, formatExtensions[inputFormat], confDirRecursively) - if len(files) == 0 { - base.Fatalf("empty config list") + m, err := helpers.LoadConfigToMap(cmd.Flag.Args(), inputFormat, confDirRecursively) + if err != nil { + base.Fatalf(err.Error()) + } + err = merge.ApplyRules(m) + if err != nil { + base.Fatalf(err.Error()) } - m := mergeConvertToMap(files, inputFormat) - var ( - out []byte - err error - ) + var out []byte switch outputFormat { - case "json": + case core.FormatJSON: out, err = json.Marshal(m) if err != nil { base.Fatalf("failed to marshal json: %s", err) } - case "toml": + case core.FormatTOML: out, err = toml.Marshal(m) if err != nil { base.Fatalf("failed to marshal json: %s", err) } - case "yaml": + case core.FormatYAML: out, err = yaml.Marshal(m) if err != nil { base.Fatalf("failed to marshal json: %s", err) } - case "pb", "protobuf": + case core.FormatProtobuf, core.FormatProtobufShort: data, err := json.Marshal(m) if err != nil { base.Fatalf("failed to marshal json: %s", err) @@ -132,6 +130,6 @@ func executeConvert(cmd *base.Command, args []string) { } if _, err := os.Stdout.Write(out); err != nil { - base.Fatalf("failed to write proto config: %s", err) + base.Fatalf("failed to write stdout: %s", err) } } diff --git a/main/commands/all/convert_confs.go b/main/commands/all/convert_confs.go deleted file mode 100644 index 567de61e4..000000000 --- a/main/commands/all/convert_confs.go +++ /dev/null @@ -1,138 +0,0 @@ -package all - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/json" - "github.com/v2fly/v2ray-core/v4/infra/conf/merge" - "github.com/v2fly/v2ray-core/v4/main/commands/base" -) - -func mergeConvertToMap(files []string, format string) map[string]interface{} { - var ( - m map[string]interface{} - err error - ) - switch inputFormat { - case "json": - m, err = merge.FilesToMap(files) - if err != nil { - base.Fatalf("failed to load json: %s", err) - } - case "toml": - bs, err := tomlsToJSONs(files) - if err != nil { - base.Fatalf("failed to convert toml to json: %s", err) - } - m, err = merge.BytesToMap(bs) - if err != nil { - base.Fatalf("failed to merge converted json: %s", err) - } - case "yaml": - bs, err := yamlsToJSONs(files) - if err != nil { - base.Fatalf("failed to convert yaml to json: %s", err) - } - m, err = merge.BytesToMap(bs) - if err != nil { - base.Fatalf("failed to merge converted json: %s", err) - } - default: - base.Errorf("invalid input format: %s", format) - base.Errorf("Run '%s help %s' for details.", base.CommandEnv.Exec, cmdConvert.LongName()) - base.Exit() - } - return m -} - -// resolveFolderToFiles expands folder path (if any and it exists) to file paths. -// Any other paths, like file, even URL, it returns them as is. -func resolveFolderToFiles(paths []string, extensions []string, recursively bool) []string { - dirReader := readConfDir - if recursively { - dirReader = readConfDirRecursively - } - files := make([]string, 0) - for _, p := range paths { - i, err := os.Stat(p) - if err == nil && i.IsDir() { - files = append(files, dirReader(p, extensions)...) - continue - } - files = append(files, p) - } - return files -} - -func readConfDir(dirPath string, extensions []string) []string { - confs, err := ioutil.ReadDir(dirPath) - if err != nil { - base.Fatalf("failed to read dir %s: %s", dirPath, err) - } - files := make([]string, 0) - for _, f := range confs { - ext := filepath.Ext(f.Name()) - for _, e := range extensions { - if strings.EqualFold(ext, e) { - files = append(files, filepath.Join(dirPath, f.Name())) - break - } - } - } - return files -} - -// getFolderFiles get files in the folder and it's children -func readConfDirRecursively(dirPath string, extensions []string) []string { - files := make([]string, 0) - err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { - ext := filepath.Ext(path) - for _, e := range extensions { - if strings.EqualFold(ext, e) { - files = append(files, path) - break - } - } - return nil - }) - if err != nil { - base.Fatalf("failed to read dir %s: %s", dirPath, err) - } - return files -} - -func yamlsToJSONs(files []string) ([][]byte, error) { - jsons := make([][]byte, 0) - for _, file := range files { - bs, err := cmdarg.LoadArgToBytes(file) - if err != nil { - return nil, err - } - j, err := json.FromYAML(bs) - if err != nil { - return nil, err - } - jsons = append(jsons, j) - } - return jsons, nil -} - -func tomlsToJSONs(files []string) ([][]byte, error) { - jsons := make([][]byte, 0) - for _, file := range files { - bs, err := cmdarg.LoadArgToBytes(file) - if err != nil { - return nil, err - } - j, err := json.FromTOML(bs) - if err != nil { - return nil, err - } - jsons = append(jsons, j) - } - return jsons, nil -} diff --git a/main/commands/all/format_doc.go b/main/commands/all/format_doc.go index d5c223530..beaaef601 100644 --- a/main/commands/all/format_doc.go +++ b/main/commands/all/format_doc.go @@ -10,44 +10,41 @@ var docFormat = &base.Command{ Long: ` {{.Exec}} supports different config formats: + * auto + The default loader, supports all extensions below. + It loads config by format detecting, with mixed + formats support. + * json (.json, .jsonc) - The default loader, multiple config files support. + The json loader, multiple files support, mergeable. * toml (.toml) - The toml loader, multiple config files support. + The toml loader, multiple files support, mergeable. * yaml (.yml) - The yaml loader, multiple config files support. + The yaml loader, multiple files support, mergeable. * protobuf / pb (.pb) - Single conifg file support. If multiple files assigned, - only the first one is loaded. + Single file support, unmergeable. -If "-format" is not explicitly specified, {{.Exec}} will choose -a loader by detecting the extension of the first config file, or -use the default loader. The following explains how format loaders behave with examples. Examples: - {{.Exec}} run -d dir (1) - {{.Exec}} run -format=protobuf -d dir (2) - {{.Exec}} test -c c1.yml -d dir (3) - {{.Exec}} test -format=pb -c c1.json (4) + {{.Exec}} run -d dir (1) + {{.Exec}} run -c c1.json -c c2.yaml (2) + {{.Exec}} run -format=json -d dir (3) + {{.Exec}} test -c c1.yml -c c2.pb (4) + {{.Exec}} test -format=pb -d dir (5) + {{.Exec}} test -format=protobuf -c c1.json (6) -(1) The default json loader is used, {{.Exec}} will try to load all - json files in the "dir". - -(2) The protobuf loader is specified, {{.Exec}} will try to find - all protobuf files in the "dir", but only the the first - .pb file is loaded. - -(3) The yaml loader is selected because of the "c1.yml" file, - {{.Exec}} will try to load "c1.yml" and all yaml files in - the "dir". - -(4) The protobuf loader is specified, {{.Exec}} will load - "c1.json" as protobuf, no matter its extension. +(1) Load all supported files in the "dir". +(2) JSON and YAML are merged and loaded. +(3) Load all JSON files in the "dir". +(4) Goes error since .pb is not mergeable to others +(5) Works only when single .pb file found, if not, failed due to + unmergeable. +(6) Force load "c1.json" as protobuf, no matter its extension. `, } diff --git a/main/commands/all/merge_doc.go b/main/commands/all/merge_doc.go index 420fca32c..2999943f8 100644 --- a/main/commands/all/merge_doc.go +++ b/main/commands/all/merge_doc.go @@ -13,6 +13,9 @@ Merging of config files is applied in following commands: {{.Exec}} run -c c1.json -c c2.json ... {{.Exec}} test -c c1.yaml -c c2.yaml ... {{.Exec}} convert c1.json dir1 ... + {{.Exec}} api ado c1.json dir1 ... + {{.Exec}} api rmi c1.json dir1 ... + ... and more ... Support of toml and yaml is implemented by converting them to json, both merge and load. So we take json as example here. diff --git a/main/commands/all/tls/cert.go b/main/commands/all/tls/cert.go index be7b2b1f0..5945d66a7 100644 --- a/main/commands/all/tls/cert.go +++ b/main/commands/all/tls/cert.go @@ -16,30 +16,30 @@ import ( // cmdCert is the tls cert command var cmdCert = &base.Command{ - UsageLine: "{{.Exec}} tls cert [--ca] [--domain=v2ray.com] [--expire=240h]", + UsageLine: "{{.Exec}} tls cert [--ca] [--domain=v2fly.org] [--expire=240h]", Short: "Generate TLS certificates", Long: ` Generate TLS certificates. Arguments: - -domain=domain_name + -domain The domain name for the certificate. - -org=organization + -org The organization name for the certificate. -ca - Whether this certificate is a CA + The certificate is a CA -json - The output of certificate to JSON + To output certificate to JSON - -file + -file The certificate path to save. - -expire - Expire time of the certificate. Default value 3 months. + -expire + Expire days of the certificate. Default 90 days. `, } @@ -54,12 +54,12 @@ var ( return true }() - certCommonName = cmdCert.Flag.String("name", "V2Ray Inc", "The common name of this certificate") - certOrganization = cmdCert.Flag.String("org", "V2Ray Inc", "Organization of the certificate") - certIsCA = cmdCert.Flag.Bool("ca", false, "Whether this certificate is a CA") - certJSONOutput = cmdCert.Flag.Bool("json", true, "Print certificate in JSON format") - certFileOutput = cmdCert.Flag.String("file", "", "Save certificate in file.") - certExpire = cmdCert.Flag.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.") + certCommonName = cmdCert.Flag.String("name", "V2Ray Inc", "") + certOrganization = cmdCert.Flag.String("org", "V2Ray Inc", "") + certIsCA = cmdCert.Flag.Bool("ca", false, "") + certJSONOutput = cmdCert.Flag.Bool("json", true, "") + certFileOutput = cmdCert.Flag.String("file", "", "") + certExpire = cmdCert.Flag.Uint("expire", 90, "") ) func executeCert(cmd *base.Command, args []string) { @@ -69,7 +69,7 @@ func executeCert(cmd *base.Command, args []string) { opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature)) } - opts = append(opts, cert.NotAfter(time.Now().Add(*certExpire))) + opts = append(opts, cert.NotAfter(time.Now().Add(time.Duration(*certExpire)*time.Hour*24))) opts = append(opts, cert.CommonName(*certCommonName)) if len(certDomainNames) > 0 { opts = append(opts, cert.DNSNames(certDomainNames...)) diff --git a/main/commands/all/tls/ping.go b/main/commands/all/tls/ping.go index 47c070d1a..1f4d4fbec 100644 --- a/main/commands/all/tls/ping.go +++ b/main/commands/all/tls/ping.go @@ -14,13 +14,13 @@ import ( // cmdPing is the tls ping command var cmdPing = &base.Command{ UsageLine: "{{.Exec}} tls ping [-ip ] ", - Short: "Ping the domain with TLS handshake", + Short: "ping the domain with TLS handshake", Long: ` Ping the domain with TLS handshake. Arguments: - -ip + -ip The IP address of the domain. `, } diff --git a/main/commands/all/uuid.go b/main/commands/all/uuid.go index 2bf528433..804edef64 100644 --- a/main/commands/all/uuid.go +++ b/main/commands/all/uuid.go @@ -9,7 +9,7 @@ import ( var cmdUUID = &base.Command{ UsageLine: "{{.Exec}} uuid", - Short: "Generate new UUIDs", + Short: "generate new UUIDs", Long: `Generate new UUIDs. `, Run: executeUUID, diff --git a/main/commands/all/verify.go b/main/commands/all/verify.go index 19db86340..a8d8110d0 100644 --- a/main/commands/all/verify.go +++ b/main/commands/all/verify.go @@ -9,13 +9,13 @@ import ( var cmdVerify = &base.Command{ UsageLine: "{{.Exec}} verify [--sig=sig-file] file", - Short: "Verify if a binary is officially signed", + Short: "verify if a binary is officially signed", Long: ` Verify if a binary is officially signed. Arguments: - -sig + -sig The path to the signature file `, } diff --git a/main/commands/helpers/config_load.go b/main/commands/helpers/config_load.go new file mode 100644 index 000000000..691a2d4bd --- /dev/null +++ b/main/commands/helpers/config_load.go @@ -0,0 +1,55 @@ +package helpers + +import ( + "bytes" + "os" + + "github.com/v2fly/v2ray-core/v4/infra/conf" + "github.com/v2fly/v2ray-core/v4/infra/conf/merge" + "github.com/v2fly/v2ray-core/v4/infra/conf/mergers" + "github.com/v2fly/v2ray-core/v4/infra/conf/serial" +) + +// LoadConfig load config files to *conf.Config, it will: +// - resolve folder to files +// - try to read stdin if no file specified +func LoadConfig(files []string, format string, recursively bool) (*conf.Config, error) { + m, err := LoadConfigToMap(files, format, recursively) + if err != nil { + return nil, err + } + bs, err := merge.FromMap(m) + if err != nil { + return nil, err + } + r := bytes.NewReader(bs) + return serial.DecodeJSONConfig(r) +} + +// LoadConfigToMap load config files to map, it will: +// - resolve folder to files +// - try to read stdin if no file specified +func LoadConfigToMap(files []string, format string, recursively bool) (map[string]interface{}, error) { + var err error + if len(files) > 0 { + var extensions []string + extensions, err := mergers.GetExtensions(format) + if err != nil { + return nil, err + } + files, err = ResolveFolderToFiles(files, extensions, recursively) + if err != nil { + return nil, err + } + } + m := make(map[string]interface{}) + if len(files) == 0 { + err = mergers.MergeAs(format, os.Stdin, m) + } else { + err = mergers.MergeAs(format, files, m) + } + if err != nil { + return nil, err + } + return m, nil +} diff --git a/main/commands/helpers/fs.go b/main/commands/helpers/fs.go new file mode 100644 index 000000000..855ee19b7 --- /dev/null +++ b/main/commands/helpers/fs.go @@ -0,0 +1,70 @@ +package helpers + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// ReadDir finds files according to extensions in the dir +func ReadDir(dir string, extensions []string) ([]string, error) { + confs, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + files := make([]string, 0) + for _, f := range confs { + ext := filepath.Ext(f.Name()) + for _, e := range extensions { + if strings.EqualFold(ext, e) { + files = append(files, filepath.Join(dir, f.Name())) + break + } + } + } + return files, nil +} + +// ReadDirRecursively finds files according to extensions in the dir recursively +func ReadDirRecursively(dir string, extensions []string) ([]string, error) { + files := make([]string, 0) + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + ext := filepath.Ext(path) + for _, e := range extensions { + if strings.EqualFold(ext, e) { + files = append(files, path) + break + } + } + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +// ResolveFolderToFiles expands folder path (if any and it exists) to file paths. +// Any other paths, like file, even URL, it returns them as is. +func ResolveFolderToFiles(paths []string, extensions []string, recursively bool) ([]string, error) { + dirReader := ReadDir + if recursively { + dirReader = ReadDirRecursively + } + files := make([]string, 0) + for _, p := range paths { + i, err := os.Stat(p) + if err == nil && i.IsDir() { + fs, err := dirReader(p, extensions) + if err != nil { + return nil, fmt.Errorf("failed to read dir %s: %s", p, err) + } + files = append(files, fs...) + continue + } + files = append(files, p) + } + return files, nil +} diff --git a/main/commands/run.go b/main/commands/run.go index 05be55abe..734ea54b3 100644 --- a/main/commands/run.go +++ b/main/commands/run.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "io/ioutil" "log" "os" @@ -20,22 +21,22 @@ import ( var CmdRun = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} run [-c config.json] [-d dir]", - Short: "Run V2Ray with config", + Short: "run V2Ray with config", Long: ` Run V2Ray with config. Arguments: - -c, -config + -c, -config Config file for V2Ray. Multiple assign is accepted. - -d, -confdir + -d, -confdir A dir with config files. Multiple assign is accepted. -r Load confdir recursively. - -format + -format Format of input files. (default "json") Examples: @@ -56,7 +57,7 @@ var ( ) func setConfigFlags(cmd *base.Command) { - configFormat = cmd.Flag.String("format", "", "") + configFormat = cmd.Flag.String("format", core.FormatAuto, "") configDirRecursively = cmd.Flag.Bool("r", false, "") cmd.Flag.Var(&configFiles, "config", "") @@ -69,6 +70,7 @@ func executeRun(cmd *base.Command, args []string) { setConfigFlags(cmd) cmd.Flag.Parse(args) printVersion() + configFiles = getConfigFilePath() server, err := startV2Ray() if err != nil { base.Fatalf("Failed to start: %s", err) @@ -139,20 +141,8 @@ func readConfDirRecursively(dirPath string, extension []string) cmdarg.Arg { return files } -func getLoaderExtension() ([]string, error) { - firstFile := "" - if len(configFiles) > 0 { - firstFile = configFiles[0] - } - loader, err := core.GetConfigLoader(*configFormat, firstFile) - if err != nil { - return nil, err - } - return loader.Extension, nil -} - func getConfigFilePath() cmdarg.Arg { - extension, err := getLoaderExtension() + extension, err := core.GetLoaderExtensions(*configFormat) if err != nil { base.Fatalf(err.Error()) } @@ -190,16 +180,18 @@ func getConfigFilePath() cmdarg.Arg { return cmdarg.Arg{configFile} } - log.Println("Using config from STDIN") - return cmdarg.Arg{"stdin:"} + return nil } func startV2Ray() (core.Server, error) { - configFiles := getConfigFilePath() - - config, err := core.LoadConfig(*configFormat, configFiles[0], configFiles) + config, err := core.LoadConfig(*configFormat, configFiles) if err != nil { - return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err) + if len(configFiles) == 0 { + err = newError("failed to load config").Base(err) + } else { + err = newError(fmt.Sprintf("failed to load config: %s", configFiles)).Base(err) + } + return nil, err } server, err := core.New(config) diff --git a/main/commands/test.go b/main/commands/test.go index 75388f0d6..b5c00a3b9 100644 --- a/main/commands/test.go +++ b/main/commands/test.go @@ -12,22 +12,22 @@ import ( var CmdTest = &base.Command{ CustomFlags: true, UsageLine: "{{.Exec}} test [-format=json] [-c config.json] [-d dir]", - Short: "Test config files", + Short: "test config files", Long: ` Test config files, without launching V2Ray server. Arguments: - -c, -config + -c, -config Config file for V2Ray. Multiple assign is accepted. - -d, -confdir + -d, -confdir A dir with config files. Multiple assign is accepted. -r Load confdir recursively. - -format + -format Format of input files. (default "json") Examples: @@ -44,7 +44,7 @@ func executeTest(cmd *base.Command, args []string) { setConfigFlags(cmd) cmd.Flag.Parse(args) - extension, err := getLoaderExtension() + extension, err := core.GetLoaderExtensions(*configFormat) if err != nil { base.Fatalf(err.Error()) } @@ -59,32 +59,10 @@ func executeTest(cmd *base.Command, args []string) { configFiles = append(configFiles, dirReader(d, extension)...) } } - if len(configFiles) == 0 { - if len(configDirs) == 0 { - cmd.Flag.Usage() - base.SetExitStatus(1) - base.Exit() - } - base.Fatalf("no config file found with extension: %s", extension) - } printVersion() - _, err = startV2RayTesting() + _, err = startV2Ray() if err != nil { base.Fatalf("Test failed: %s", err) } fmt.Println("Configuration OK.") } - -func startV2RayTesting() (core.Server, error) { - config, err := core.LoadConfig(*configFormat, configFiles[0], configFiles) - if err != nil { - return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err) - } - - server, err := core.New(config) - if err != nil { - return nil, newError("failed to create server").Base(err) - } - - return server, nil -} diff --git a/main/commands/version.go b/main/commands/version.go index 627ffe0d6..988a95573 100644 --- a/main/commands/version.go +++ b/main/commands/version.go @@ -10,7 +10,7 @@ import ( // CmdVersion prints V2Ray Versions var CmdVersion = &base.Command{ UsageLine: "{{.Exec}} version", - Short: "Print V2Ray Versions", + Short: "print V2Ray version", Long: `Prints the build information for V2Ray. `, Run: executeVersion, diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 9eed3c132..f33032edd 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -70,17 +70,8 @@ import ( _ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/memconservative" _ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard" - // JSON config support. Choose only one from the two below. - // The following line loads JSON from v2ctl - // _ "github.com/v2fly/v2ray-core/v4/main/json" - // The following line loads JSON internally - _ "github.com/v2fly/v2ray-core/v4/main/json" - - // TOML config support. - _ "github.com/v2fly/v2ray-core/v4/main/toml" - - // YAML config support. - _ "github.com/v2fly/v2ray-core/v4/main/yaml" + // JSON, TOML, YAML config support. + _ "github.com/v2fly/v2ray-core/v4/main/formats" // commands _ "github.com/v2fly/v2ray-core/v4/main/commands/all" diff --git a/main/formats/errors.generated.go b/main/formats/errors.generated.go new file mode 100644 index 000000000..4b53f3147 --- /dev/null +++ b/main/formats/errors.generated.go @@ -0,0 +1,9 @@ +package formats + +import "github.com/v2fly/v2ray-core/v4/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/main/formats/formats.go b/main/formats/formats.go new file mode 100644 index 000000000..afd53719b --- /dev/null +++ b/main/formats/formats.go @@ -0,0 +1,56 @@ +package formats + +import ( + "bytes" + + core "github.com/v2fly/v2ray-core/v4" + "github.com/v2fly/v2ray-core/v4/common" + "github.com/v2fly/v2ray-core/v4/infra/conf/merge" + "github.com/v2fly/v2ray-core/v4/infra/conf/mergers" + "github.com/v2fly/v2ray-core/v4/infra/conf/serial" +) + +func init() { + for _, formatName := range mergers.GetAllNames() { + loader, err := makeMergeLoader(formatName) + if err != nil { + panic(err) + } + if formatName == core.FormatAuto { + loader.Extension = nil + } + common.Must(core.RegisterConfigLoader(loader)) + } +} + +func makeMergeLoader(formatName string) (*core.ConfigFormat, error) { + extenstoins, err := mergers.GetExtensions(formatName) + if err != nil { + return nil, err + } + return &core.ConfigFormat{ + Name: []string{formatName}, + Extension: extenstoins, + Loader: makeLoaderFunc(formatName), + }, nil +} + +func makeLoaderFunc(formatName string) core.ConfigLoader { + return func(input interface{}) (*core.Config, error) { + m := make(map[string]interface{}) + err := mergers.MergeAs(formatName, input, m) + if err != nil { + return nil, err + } + data, err := merge.FromMap(m) + if err != nil { + return nil, err + } + r := bytes.NewReader(data) + cf, err := serial.DecodeJSONConfig(r) + if err != nil { + return nil, err + } + return cf.Build() + } +} diff --git a/main/json/json.go b/main/json/json.go deleted file mode 100644 index 1e019ede1..000000000 --- a/main/json/json.go +++ /dev/null @@ -1,38 +0,0 @@ -package json - -import ( - "bytes" - "io" - - core "github.com/v2fly/v2ray-core/v4" - "github.com/v2fly/v2ray-core/v4/common" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/merge" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" -) - -func init() { - common.Must(core.RegisterConfigLoader(&core.ConfigFormat{ - Name: []string{"JSON"}, - Extension: []string{".json", ".jsonc"}, - Loader: func(input interface{}) (*core.Config, error) { - switch v := input.(type) { - case cmdarg.Arg: - data, err := merge.FilesToJSON(v) - if err != nil { - return nil, err - } - r := bytes.NewReader(data) - cf, err := serial.DecodeJSONConfig(r) - if err != nil { - return nil, err - } - return cf.Build() - case io.Reader: - return serial.LoadJSONConfig(v) - default: - return nil, newError("unknow type") - } - }, - })) -} diff --git a/main/toml/toml.go b/main/toml/toml.go deleted file mode 100644 index 29fc0e984..000000000 --- a/main/toml/toml.go +++ /dev/null @@ -1,69 +0,0 @@ -package toml - -import ( - "bytes" - "errors" - "io" - "io/ioutil" - - "github.com/v2fly/v2ray-core/v4" - "github.com/v2fly/v2ray-core/v4/common" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/json" - "github.com/v2fly/v2ray-core/v4/infra/conf/merge" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" -) - -func init() { - common.Must(core.RegisterConfigLoader(&core.ConfigFormat{ - Name: []string{"TOML"}, - Extension: []string{".toml"}, - Loader: func(input interface{}) (*core.Config, error) { - switch v := input.(type) { - case cmdarg.Arg: - bs, err := tomlsToJSONs(v) - if err != nil { - return nil, err - } - data, err := merge.BytesToJSON(bs) - if err != nil { - return nil, err - } - r := bytes.NewReader(data) - cf, err := serial.DecodeJSONConfig(r) - if err != nil { - return nil, err - } - return cf.Build() - case io.Reader: - bs, err := ioutil.ReadAll(v) - if err != nil { - return nil, err - } - bs, err = json.FromTOML(bs) - if err != nil { - return nil, err - } - return serial.LoadJSONConfig(bytes.NewBuffer(bs)) - default: - return nil, errors.New("unknow type") - } - }, - })) -} - -func tomlsToJSONs(files []string) ([][]byte, error) { - jsons := make([][]byte, 0) - for _, file := range files { - bs, err := cmdarg.LoadArgToBytes(file) - if err != nil { - return nil, err - } - j, err := json.FromTOML(bs) - if err != nil { - return nil, err - } - jsons = append(jsons, j) - } - return jsons, nil -} diff --git a/main/yaml/yaml.go b/main/yaml/yaml.go deleted file mode 100644 index 0b5ae2ad4..000000000 --- a/main/yaml/yaml.go +++ /dev/null @@ -1,69 +0,0 @@ -package yaml - -import ( - "bytes" - "errors" - "io" - "io/ioutil" - - core "github.com/v2fly/v2ray-core/v4" - "github.com/v2fly/v2ray-core/v4/common" - "github.com/v2fly/v2ray-core/v4/common/cmdarg" - "github.com/v2fly/v2ray-core/v4/infra/conf/json" - "github.com/v2fly/v2ray-core/v4/infra/conf/merge" - "github.com/v2fly/v2ray-core/v4/infra/conf/serial" -) - -func init() { - common.Must(core.RegisterConfigLoader(&core.ConfigFormat{ - Name: []string{"YAML"}, - Extension: []string{".yml", ".yaml"}, - Loader: func(input interface{}) (*core.Config, error) { - switch v := input.(type) { - case cmdarg.Arg: - bs, err := yamlsToJSONs(v) - if err != nil { - return nil, err - } - data, err := merge.BytesToJSON(bs) - if err != nil { - return nil, err - } - r := bytes.NewReader(data) - cf, err := serial.DecodeJSONConfig(r) - if err != nil { - return nil, err - } - return cf.Build() - case io.Reader: - bs, err := ioutil.ReadAll(v) - if err != nil { - return nil, err - } - bs, err = json.FromYAML(bs) - if err != nil { - return nil, err - } - return serial.LoadJSONConfig(bytes.NewBuffer(bs)) - default: - return nil, errors.New("unknow type") - } - }, - })) -} - -func yamlsToJSONs(files []string) ([][]byte, error) { - jsons := make([][]byte, 0) - for _, file := range files { - bs, err := cmdarg.LoadArgToBytes(file) - if err != nil { - return nil, err - } - j, err := json.FromYAML(bs) - if err != nil { - return nil, err - } - jsons = append(jsons, j) - } - return jsons, nil -} diff --git a/testing/scenarios/common_coverage.go b/testing/scenarios/common_coverage.go index 03ff93bc5..71323410f 100644 --- a/testing/scenarios/common_coverage.go +++ b/testing/scenarios/common_coverage.go @@ -28,7 +28,7 @@ func RunV2RayProtobuf(config []byte) *exec.Cmd { os.MkdirAll(covDir, os.ModeDir) randomID := uuid.New() profile := randomID.String() + ".out" - proc := exec.Command(testBinaryPath, "run", "-config=stdin:", "-format=pb", "-test.run", "TestRunMainForCoverage", "-test.coverprofile", profile, "-test.outputdir", covDir) + proc := exec.Command(testBinaryPath, "run", "-format=pb", "-test.run", "TestRunMainForCoverage", "-test.coverprofile", profile, "-test.outputdir", covDir) proc.Stdin = bytes.NewBuffer(config) proc.Stderr = os.Stderr proc.Stdout = os.Stdout diff --git a/testing/scenarios/common_regular.go b/testing/scenarios/common_regular.go index a254b9caa..f42ce4335 100644 --- a/testing/scenarios/common_regular.go +++ b/testing/scenarios/common_regular.go @@ -23,7 +23,7 @@ func BuildV2Ray() error { func RunV2RayProtobuf(config []byte) *exec.Cmd { genTestBinaryPath() - proc := exec.Command(testBinaryPath, "run", "-config=stdin:", "-format=pb") + proc := exec.Command(testBinaryPath, "run", "-format=pb") proc.Stdin = bytes.NewBuffer(config) proc.Stderr = os.Stderr proc.Stdout = os.Stdout diff --git a/v2ray_test.go b/v2ray_test.go index f3604b0f7..5bec5b2c1 100644 --- a/v2ray_test.go +++ b/v2ray_test.go @@ -85,7 +85,7 @@ func TestV2RayClose(t *testing.T) { cfgBytes, err := proto.Marshal(config) common.Must(err) - server, err := StartInstance("protobuf", cfgBytes) + server, err := StartInstance(FormatProtobuf, cfgBytes) common.Must(err) server.Close() }