Browse Source

improve commands
(rebased from ebbf31f07e67b2081de07c2021871ca50758d014)

Jebbs 4 years ago
parent
commit
2523d77919
67 changed files with 1715 additions and 1212 deletions
  1. 31 0
      app/log/command/command.go
  2. 146 22
      app/log/command/config.pb.go
  3. 7 0
      app/log/command/config.proto
  4. 65 1
      app/log/command/config_grpc.pb.go
  5. 23 0
      app/log/log.go
  6. 1 1
      app/router/balancing.go
  7. 10 10
      app/router/balancing_override.go
  8. 15 4
      app/stats/command/command.go
  9. 79 58
      app/stats/command/command.pb.go
  10. 3 0
      app/stats/command/command.proto
  11. 0 3
      common/cmdarg/arg.go
  12. 6 0
      common/log/log.go
  13. 98 0
      common/units/bytesize.go
  14. 66 0
      common/units/bytesize_test.go
  15. 110 33
      config.go
  16. 1 1
      features/routing/health.go
  17. 1 1
      functions.go
  18. 3 3
      functions_test.go
  19. 1 0
      infra/conf/merge/map.go
  20. 23 72
      infra/conf/merge/merge.go
  21. 14 10
      infra/conf/merge/merge_test.go
  22. 2 1
      infra/conf/merge/rules.go
  23. 1 1
      infra/conf/mergers/errors.generated.go
  24. 25 0
      infra/conf/mergers/extensions.go
  25. 40 0
      infra/conf/mergers/formats.go
  26. 95 0
      infra/conf/mergers/merge.go
  27. 103 0
      infra/conf/mergers/merger_base.go
  28. 32 0
      infra/conf/mergers/mergers.go
  29. 10 0
      infra/conf/mergers/names.go
  30. 3 5
      main/commands/all/api/api.go
  31. 2 2
      main/commands/all/api/balancer_check.go
  32. 23 12
      main/commands/all/api/balancer_info.go
  33. 17 16
      main/commands/all/api/balancer_override.go
  34. 19 27
      main/commands/all/api/inbounds_add.go
  35. 21 34
      main/commands/all/api/inbounds_remove.go
  36. 83 0
      main/commands/all/api/log.go
  37. 0 40
      main/commands/all/api/logger_restart.go
  38. 19 27
      main/commands/all/api/outbounds_add.go
  39. 21 33
      main/commands/all/api/outbounds_remove.go
  40. 40 49
      main/commands/all/api/shared.go
  41. 0 140
      main/commands/all/api/shared_test.go
  42. 165 0
      main/commands/all/api/stats.go
  43. 0 55
      main/commands/all/api/stats_get.go
  44. 0 55
      main/commands/all/api/stats_query.go
  45. 0 40
      main/commands/all/api/stats_sys.go
  46. 27 29
      main/commands/all/convert.go
  47. 0 138
      main/commands/all/convert_confs.go
  48. 23 26
      main/commands/all/format_doc.go
  49. 3 0
      main/commands/all/merge_doc.go
  50. 15 15
      main/commands/all/tls/cert.go
  51. 2 2
      main/commands/all/tls/ping.go
  52. 1 1
      main/commands/all/uuid.go
  53. 2 2
      main/commands/all/verify.go
  54. 55 0
      main/commands/helpers/config_load.go
  55. 70 0
      main/commands/helpers/fs.go
  56. 16 24
      main/commands/run.go
  57. 6 28
      main/commands/test.go
  58. 1 1
      main/commands/version.go
  59. 2 11
      main/distro/all/all.go
  60. 9 0
      main/formats/errors.generated.go
  61. 56 0
      main/formats/formats.go
  62. 0 38
      main/json/json.go
  63. 0 69
      main/toml/toml.go
  64. 0 69
      main/yaml/yaml.go
  65. 1 1
      testing/scenarios/common_coverage.go
  66. 1 1
      testing/scenarios/common_regular.go
  67. 1 1
      v2ray_test.go

+ 31 - 0
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 {

+ 146 - 22
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,
 		},

+ 7 - 0
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) {};
 }

+ 65 - 1
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",
 }

+ 23 - 0
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 {

+ 1 - 1
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

+ 10 - 10
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

+ 15 - 4
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)

+ 79 - 58
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 (

+ 3 - 0
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 {

+ 0 - 3
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)
 	}

+ 6 - 0
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

+ 98 - 0
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
+}

+ 66 - 0
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
+}

+ 110 - 33
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
+	return nil, newError("config loader not found: ", formatName).AtWarning()
+}
+
+// GetAllExtensions get all extensions supported
+func GetAllExtensions() []string {
+	extensions := make([]string, 0)
+	for _, f := range configLoaderByName {
+		extensions = append(extensions, f.Extension...)
 	}
-	panic("default loader not found")
+	return extensions
 }
 
-// LoadConfig loads config with given format from given source.
-// input accepts 2 different types:
+// 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
 				}

+ 1 - 1
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

+ 1 - 1
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
 	}

+ 3 - 3
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()
 

+ 1 - 0
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)

+ 23 - 72
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
+// JSONs merges multiple json contents into one json.
+func JSONs(args [][]byte) ([]byte, error) {
+	m := make(map[string]interface{})
+	for _, arg := range args {
+		if _, err := ToMap(arg, m); err != nil {
+			return nil, err
+		}
 	}
-	return json.Marshal(m)
+	return FromMap(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
+// 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{})
 	}
-	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)
+	r := bytes.NewReader(content)
+	n := make(map[string]interface{})
+	err := serial.DecodeJSON(r, &n)
 	if err != nil {
 		return nil, err
 	}
-	err = applyRules(m)
-	if err != nil {
+	if err = mergeMaps(target, n); err != nil {
 		return nil, err
 	}
-	return m, nil
-}
-
-func loadFiles(args []string) (map[string]interface{}, error) {
-	c := 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
+	return target, 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 {
-			return nil, err
-		}
+// 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{})
 	}
-	return conf, nil
-}
-
-func decode(r io.Reader) (map[string]interface{}, error) {
-	c := make(map[string]interface{})
-	err := serial.DecodeJSON(r, &c)
+	err := ApplyRules(target)
 	if err != nil {
 		return nil, err
 	}
-	return c, nil
+	return json.Marshal(target)
 }

+ 14 - 10
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) {
+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)
+	}
 	e := make(map[string]interface{})
-	err := serial.DecodeJSON(strings.NewReader(expected), &e)
+	err = serial.DecodeJSON(strings.NewReader(expected), &e)
 	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))
+	if !reflect.DeepEqual(v, e) {
+		t.Fatalf("expected:\n%s\n\nactual:\n%s", expected, string(value))
 	}
 }

+ 2 - 1
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

+ 1 - 1
main/json/errors.generated.go → infra/conf/mergers/errors.generated.go

@@ -1,4 +1,4 @@
-package json
+package mergers
 
 import "github.com/v2fly/v2ray-core/v4/common/errors"
 

+ 25 - 0
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
+}

+ 40 - 0
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
+}

+ 95 - 0
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)
+}

+ 103 - 0
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
+}

+ 32 - 0
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,
+		}),
+	)
+}

+ 10 - 0
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
+}

+ 3 - 5
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,

+ 2 - 2
main/commands/all/api/balancer_check.go

@@ -18,10 +18,10 @@ of server config.
 
 Arguments:
 
-	-s, -server 
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 
 Example:

+ 23 - 12
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 <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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')
 }

+ 17 - 16
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:
 
-	-r, -remove
-		Remove the overridden
+	-b, -balancer <tag>
+		Tag of the target balancer. Required.
 
-	-b, -balancer
-		Tag of the balancer. Required
+	-v, -validity <duration>
+		Time duration of the validity of override, e.g.: 60s, 60m, 
+		24h, 1m30s. Default 1h.
 
-	-v, -validity
-		Time minutes of the validity of overridden. Default 60
+	-r, -remove
+		Remove the override
 
-	-s, -server 
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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)
 	}
 }

+ 19 - 27
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] <c1.json> [c2.json]...",
-	Short:       "Add inbounds",
+	Short:       "add inbounds",
 	Long: `
 Add inbounds to V2Ray.
 
 Arguments:
 
-	-s, -server 
+	-format <format>
+		Specify the input format.
+		Available values: "auto", "json", "toml", "yaml"
+		Default: "auto"
+
+	-r
+		Load folders recursively.
+
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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:"}
-	}
-
-	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...)
+	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+	if err != nil {
+		base.Fatalf("%s", err)
 	}
-	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)
 	}
 }

+ 21 - 34
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> [json_file] [tag]...",
-	Short:       "Remove inbounds",
+	Short:       "remove inbounds",
 	Long: `
 Remove inbounds from V2Ray.
 
 Arguments:
 
-	-s, -server 
+	-format <format>
+		Specify the input format.
+		Available values: "auto", "json", "toml", "yaml"
+		Default: "auto"
+
+	-r
+		Load folders recursively.
+
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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:"}
-	}
-
-	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)
-		}
+	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+	if err != nil {
+		base.Fatalf("%s", err)
 	}
-
-	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)
 	}
 }

+ 83 - 0
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 <server:port>
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout <seconds>
+		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)
+	}
+}

+ 0 - 40
main/commands/all/api/logger_restart.go

@@ -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)
-}

+ 19 - 27
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] <c1.json> [c2.json]...",
-	Short:       "Add outbounds",
+	Short:       "add outbounds",
 	Long: `
 Add outbounds to V2Ray.
 
 Arguments:
 
-	-s, -server 
+	-format <format>
+		Specify the input format.
+		Available values: "auto", "json", "toml", "yaml"
+		Default: "auto"
+
+	-r
+		Load folders recursively.
+
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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:"}
-	}
-
-	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...)
+	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+	if err != nil {
+		base.Fatalf("%s", err)
 	}
-	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)
 	}
 }

+ 21 - 33
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> [json_file] [tag]...",
-	Short:       "Remove outbounds",
+	Short:       "remove outbounds",
 	Long: `
 Remove outbounds from V2Ray.
 
 Arguments:
 
-	-s, -server 
+	-format <format>
+		Specify the input format.
+		Available values: "auto", "json", "toml", "yaml"
+		Default: "auto"
+
+	-r
+		Load folders recursively.
+
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		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:"}
-	}
-
-	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)
-		}
+	setSharedConfigFlags(cmd)
+	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+	if err != nil {
+		base.Fatalf("%s", err)
 	}
-
-	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)
 	}
 }

+ 40 - 49
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
-	}
-	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
-		}
+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)
 	}
-	return true
+	fmt.Println(output)
 }

+ 0 - 140
main/commands/all/api/shared_test.go

@@ -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)
-	}
-}

+ 165 - 0
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 <server:port>
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout <seconds>
+		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(&regexp, "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())
+}

+ 0 - 55
main/commands/all/api/stats_get.go

@@ -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)
-}

+ 0 - 55
main/commands/all/api/stats_query.go

@@ -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)
-}

+ 0 - 40
main/commands/all/api/stats_sys.go

@@ -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)
-}

+ 27 - 29
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] [<url>.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 <format>
 		Specify the input format.
-		Available values: "json", "toml", "yaml"
-		Default: "json"
+		Available values: "auto", "json", "toml", "yaml"
+		Default: "auto"
 
-	-o, -output
+	-o, -output <format>
 		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)
 	}
 }

+ 0 - 138
main/commands/all/convert_confs.go

@@ -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
-}

+ 23 - 26
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)
-
-(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.
+	{{.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) 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.
 `,
 }

+ 3 - 0
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.

+ 15 - 15
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 <domain_name>
 		The domain name for the certificate.
 
-	-org=organization 
+	-org <organization>
 		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 <path>
 		The certificate path to save.
 
-	-expire 
-		Expire time of the certificate. Default value 3 months.
+	-expire <days>
+		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...))

+ 2 - 2
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 <ip>] <domain>",
-	Short:     "Ping the domain with TLS handshake",
+	Short:     "ping the domain with TLS handshake",
 	Long: `
 Ping the domain with TLS handshake.
 
 Arguments:
 
-	-ip
+	-ip <ip>
 		The IP address of the domain.
 `,
 }

+ 1 - 1
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,

+ 2 - 2
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 <signature_file>
 		The path to the signature file
 `,
 }

+ 55 - 0
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
+}

+ 70 - 0
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
+}

+ 16 - 24
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 <file>
 		Config file for V2Ray. Multiple assign is accepted.
 
-	-d, -confdir
+	-d, -confdir <dir>
 		A dir with config files. Multiple assign is accepted.
 
 	-r
 		Load confdir recursively.
 
-	-format
+	-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)

+ 6 - 28
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 <file>
 		Config file for V2Ray. Multiple assign is accepted.
 
-	-d, -confdir
+	-d, -confdir <dir>
 		A dir with config files. Multiple assign is accepted.
 
 	-r
 		Load confdir recursively.
 
-	-format
+	-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
-}

+ 1 - 1
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,

+ 2 - 11
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"

+ 9 - 0
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{})
+}

+ 56 - 0
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()
+	}
+}

+ 0 - 38
main/json/json.go

@@ -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")
-			}
-		},
-	}))
-}

+ 0 - 69
main/toml/toml.go

@@ -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
-}

+ 0 - 69
main/yaml/yaml.go

@@ -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
-}

+ 1 - 1
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

+ 1 - 1
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

+ 1 - 1
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()
 }