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 (
 import (
 	"context"
 	"context"
+	"time"
 
 
 	grpc "google.golang.org/grpc"
 	grpc "google.golang.org/grpc"
 
 
 	core "github.com/v2fly/v2ray-core/v4"
 	core "github.com/v2fly/v2ray-core/v4"
 	"github.com/v2fly/v2ray-core/v4/app/log"
 	"github.com/v2fly/v2ray-core/v4/app/log"
 	"github.com/v2fly/v2ray-core/v4/common"
 	"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 {
 type LoggerServer struct {
 	V *core.Instance
 	V *core.Instance
 }
 }
@@ -31,6 +34,34 @@ func (s *LoggerServer) RestartLogger(ctx context.Context, request *RestartLogger
 	return &RestartLoggerResponse{}, nil
 	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() {}
 func (s *LoggerServer) mustEmbedUnimplementedLoggerServiceServer() {}
 
 
 type service struct {
 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}
 	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 protoreflect.FileDescriptor
 
 
 var file_app_log_command_config_proto_rawDesc = []byte{
 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,
 	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,
 	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,
 	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 (
 var (
@@ -175,17 +271,21 @@ func file_app_log_command_config_proto_rawDescGZIP() []byte {
 	return file_app_log_command_config_proto_rawDescData
 	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{}{
 var file_app_log_command_config_proto_goTypes = []interface{}{
 	(*Config)(nil),                // 0: v2ray.core.app.log.command.Config
 	(*Config)(nil),                // 0: v2ray.core.app.log.command.Config
 	(*RestartLoggerRequest)(nil),  // 1: v2ray.core.app.log.command.RestartLoggerRequest
 	(*RestartLoggerRequest)(nil),  // 1: v2ray.core.app.log.command.RestartLoggerRequest
 	(*RestartLoggerResponse)(nil), // 2: v2ray.core.app.log.command.RestartLoggerResponse
 	(*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{
 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
 	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 type_name
 	0, // [0:0] is the sub-list for extension extendee
 	0, // [0:0] is the sub-list for extension extendee
 	0, // [0:0] is the sub-list for field type_name
 	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
 				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{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 	out := protoimpl.TypeBuilder{
@@ -240,7 +364,7 @@ func file_app_log_command_config_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_app_log_command_config_proto_rawDesc,
 			RawDescriptor: file_app_log_command_config_proto_rawDesc,
 			NumEnums:      0,
 			NumEnums:      0,
-			NumMessages:   3,
+			NumMessages:   5,
 			NumExtensions: 0,
 			NumExtensions: 0,
 			NumServices:   1,
 			NumServices:   1,
 		},
 		},

+ 7 - 0
app/log/command/config.proto

@@ -12,6 +12,13 @@ message RestartLoggerRequest {}
 
 
 message RestartLoggerResponse {}
 message RestartLoggerResponse {}
 
 
+message FollowLogRequest {}
+
+message FollowLogResponse {
+  string message = 1;
+}
+
 service LoggerService {
 service LoggerService {
   rpc RestartLogger(RestartLoggerRequest) returns (RestartLoggerResponse) {}
   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.
 // 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 {
 type LoggerServiceClient interface {
 	RestartLogger(ctx context.Context, in *RestartLoggerRequest, opts ...grpc.CallOption) (*RestartLoggerResponse, error)
 	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 {
 type loggerServiceClient struct {
@@ -38,11 +39,44 @@ func (c *loggerServiceClient) RestartLogger(ctx context.Context, in *RestartLogg
 	return out, nil
 	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.
 // LoggerServiceServer is the server API for LoggerService service.
 // All implementations must embed UnimplementedLoggerServiceServer
 // All implementations must embed UnimplementedLoggerServiceServer
 // for forward compatibility
 // for forward compatibility
 type LoggerServiceServer interface {
 type LoggerServiceServer interface {
 	RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error)
 	RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error)
+	FollowLog(*FollowLogRequest, LoggerService_FollowLogServer) error
 	mustEmbedUnimplementedLoggerServiceServer()
 	mustEmbedUnimplementedLoggerServiceServer()
 }
 }
 
 
@@ -53,6 +87,9 @@ type UnimplementedLoggerServiceServer struct {
 func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) {
 func (UnimplementedLoggerServiceServer) RestartLogger(context.Context, *RestartLoggerRequest) (*RestartLoggerResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method RestartLogger not implemented")
 	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() {}
 func (UnimplementedLoggerServiceServer) mustEmbedUnimplementedLoggerServiceServer() {}
 
 
 // UnsafeLoggerServiceServer may be embedded to opt out of forward compatibility for this service.
 // 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)
 	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.
 // LoggerService_ServiceDesc is the grpc.ServiceDesc for LoggerService service.
 // It's only intended for direct use with grpc.RegisterService,
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
 // and not to be introspected or modified (even as a copy)
@@ -96,6 +154,12 @@ var LoggerService_ServiceDesc = grpc.ServiceDesc{
 			Handler:    _LoggerService_RestartLogger_Handler,
 			Handler:    _LoggerService_RestartLogger_Handler,
 		},
 		},
 	},
 	},
-	Streams:  []grpc.StreamDesc{},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "FollowLog",
+			Handler:       _LoggerService_FollowLog_Handler,
+			ServerStreams: true,
+		},
+	},
 	Metadata: "app/log/command/config.proto",
 	Metadata: "app/log/command/config.proto",
 }
 }

+ 23 - 0
app/log/log.go

@@ -4,6 +4,7 @@ package log
 
 
 import (
 import (
 	"context"
 	"context"
+	"reflect"
 	"sync"
 	"sync"
 
 
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/common"
@@ -16,6 +17,7 @@ type Instance struct {
 	config       *Config
 	config       *Config
 	accessLogger log.Handler
 	accessLogger log.Handler
 	errorLogger  log.Handler
 	errorLogger  log.Handler
+	followers    map[reflect.Value]func(msg log.Message)
 	active       bool
 	active       bool
 }
 }
 
 
@@ -89,6 +91,23 @@ func (g *Instance) Start() error {
 	return g.startInternal()
 	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.
 // Handle implements log.Handler.
 func (g *Instance) Handle(msg log.Message) {
 func (g *Instance) Handle(msg log.Message) {
 	g.RLock()
 	g.RLock()
@@ -98,6 +117,10 @@ func (g *Instance) Handle(msg log.Message) {
 		return
 		return
 	}
 	}
 
 
+	for _, f := range g.followers {
+		f(msg)
+	}
+
 	switch msg := msg.(type) {
 	switch msg := msg.(type) {
 	case *log.AccessMessage:
 	case *log.AccessMessage:
 		if g.accessLogger != nil {
 		if g.accessLogger != nil {

+ 1 - 1
app/router/balancing.go

@@ -21,7 +21,7 @@ type Balancer struct {
 	ohm         outbound.Manager
 	ohm         outbound.Manager
 	fallbackTag string
 	fallbackTag string
 
 
-	override overridden
+	override override
 }
 }
 
 
 // PickOutbound picks the tag of a outbound
 // 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
 	return nil
 }
 }
 
 
-type overriddenSettings struct {
+type overrideSettings struct {
 	selects []string
 	selects []string
 	until   time.Time
 	until   time.Time
 }
 }
 
 
-type overridden struct {
+type override struct {
 	access   sync.RWMutex
 	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()
 	o.access.RLock()
 	defer o.access.RUnlock()
 	defer o.access.RUnlock()
 	if len(o.settings.selects) == 0 || time.Now().After(o.settings.until) {
 	if len(o.settings.selects) == 0 || time.Now().After(o.settings.until) {
 		return nil
 		return nil
 	}
 	}
-	return &overriddenSettings{
+	return &overrideSettings{
 		selects: o.settings.selects,
 		selects: o.settings.selects,
 		until:   o.settings.until,
 		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()
 	o.access.Lock()
 	defer o.access.Unlock()
 	defer o.access.Unlock()
 	o.settings.selects = selects
 	o.settings.selects = selects
 	o.settings.until = until
 	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()
 	o.access.Lock()
 	defer o.access.Unlock()
 	defer o.access.Unlock()
 	o.settings.selects = nil
 	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) {
 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{}
 	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 {
 	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
 			var value int64
 			if request.Reset_ {
 			if request.Reset_ {
 				value = c.Set(0)
 				value = c.Set(0)

+ 79 - 58
app/stats/command/command.pb.go

@@ -184,8 +184,11 @@ type QueryStatsRequest struct {
 	sizeCache     protoimpl.SizeCache
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 	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() {
 func (x *QueryStatsRequest) Reset() {
@@ -234,6 +237,20 @@ func (x *QueryStatsRequest) GetReset_() bool {
 	return false
 	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 {
 type QueryStatsResponse struct {
 	state         protoimpl.MessageState
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	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,
 	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,
 	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,
 	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,
 	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,
 	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,
 	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,
 	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 (
 var (

+ 3 - 0
app/stats/command/command.proto

@@ -23,8 +23,11 @@ message GetStatsResponse {
 }
 }
 
 
 message QueryStatsRequest {
 message QueryStatsRequest {
+  // Deprecated, use Patterns instead
   string pattern = 1;
   string pattern = 1;
   bool reset = 2;
   bool reset = 2;
+  repeated string patterns = 3;
+  bool regexp = 4;
 }
 }
 
 
 message QueryStatsResponse {
 message QueryStatsResponse {

+ 0 - 3
common/cmdarg/arg.go

@@ -6,7 +6,6 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
-	"os"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -28,8 +27,6 @@ func LoadArgToBytes(arg string) (out []byte, err error) {
 	switch {
 	switch {
 	case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
 	case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
 		out, err = FetchHTTPContent(arg)
 		out, err = FetchHTTPContent(arg)
-	case (arg == "stdin:"):
-		out, err = ioutil.ReadAll(os.Stdin)
 	default:
 	default:
 		out, err = ioutil.ReadFile(arg)
 		out, err = ioutil.ReadFile(arg)
 	}
 	}

+ 6 - 0
common/log/log.go

@@ -16,6 +16,12 @@ type Handler interface {
 	Handle(msg Message)
 	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.
 // GeneralMessage is a general log message that can contain all kind of content.
 type GeneralMessage struct {
 type GeneralMessage struct {
 	Severity Severity
 	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
 package core
 
 
 import (
 import (
+	"fmt"
 	"io"
 	"io"
+	"log"
+	"os"
 	"path/filepath"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"strings"
 
 
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/proto"
@@ -12,6 +16,21 @@ import (
 	"github.com/v2fly/v2ray-core/v4/common/cmdarg"
 	"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.
 // ConfigFormat is a configurable format of V2Ray config file.
 type ConfigFormat struct {
 type ConfigFormat struct {
 	Name      []string
 	Name      []string
@@ -23,6 +42,7 @@ type ConfigFormat struct {
 type ConfigLoader func(input interface{}) (*Config, error)
 type ConfigLoader func(input interface{}) (*Config, error)
 
 
 var (
 var (
+	configLoaders      = make([]*ConfigFormat, 0)
 	configLoaderByName = make(map[string]*ConfigFormat)
 	configLoaderByName = make(map[string]*ConfigFormat)
 	configLoaderByExt  = make(map[string]*ConfigFormat)
 	configLoaderByExt  = make(map[string]*ConfigFormat)
 )
 )
@@ -30,11 +50,10 @@ var (
 // RegisterConfigLoader add a new ConfigLoader.
 // RegisterConfigLoader add a new ConfigLoader.
 func RegisterConfigLoader(format *ConfigFormat) error {
 func RegisterConfigLoader(format *ConfigFormat) error {
 	for _, name := range format.Name {
 	for _, name := range format.Name {
-		lname := strings.ToLower(name)
-		if _, found := configLoaderByName[lname]; found {
+		if _, found := configLoaderByName[name]; found {
 			return newError(name, " already registered.")
 			return newError(name, " already registered.")
 		}
 		}
-		configLoaderByName[lname] = format
+		configLoaderByName[name] = format
 	}
 	}
 
 
 	for _, ext := range format.Extension {
 	for _, ext := range format.Extension {
@@ -44,7 +63,7 @@ func RegisterConfigLoader(format *ConfigFormat) error {
 		}
 		}
 		configLoaderByExt[lext] = format
 		configLoaderByExt[lext] = format
 	}
 	}
-
+	configLoaders = append(configLoaders, format)
 	return nil
 	return nil
 }
 }
 
 
@@ -53,43 +72,101 @@ func getExtension(filename string) string {
 	return strings.ToLower(ext)
 	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
 // * []string slice of multiple filename/url(s) to open to read
 // * io.Reader that reads a config content (the original way)
 // * 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)
 	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) {
 func loadProtobufConfig(data []byte) (*Config, error) {
 	config := new(Config)
 	config := new(Config)
 	if err := proto.Unmarshal(data, config); err != nil {
 	if err := proto.Unmarshal(data, config); err != nil {
@@ -100,12 +177,12 @@ func loadProtobufConfig(data []byte) (*Config, error) {
 
 
 func init() {
 func init() {
 	common.Must(RegisterConfigLoader(&ConfigFormat{
 	common.Must(RegisterConfigLoader(&ConfigFormat{
-		Name:      []string{"Protobuf", "pb"},
+		Name:      []string{FormatProtobuf, FormatProtobufShort},
 		Extension: []string{".pb"},
 		Extension: []string{".pb"},
 		Loader: func(input interface{}) (*Config, error) {
 		Loader: func(input interface{}) (*Config, error) {
 			switch v := input.(type) {
 			switch v := input.(type) {
-			case cmdarg.Arg:
-				r, err := cmdarg.LoadArg(v[0])
+			case string:
+				r, err := cmdarg.LoadArg(v)
 				if err != nil {
 				if err != nil {
 					return nil, err
 					return nil, err
 				}
 				}

+ 1 - 1
features/routing/health.go

@@ -26,7 +26,7 @@ type StrategyInfo struct {
 	Others      []*OutboundInfo // Other outbounds
 	Others      []*OutboundInfo // Other outbounds
 }
 }
 
 
-// BalancingOverrideInfo holds balancing overridden information
+// BalancingOverrideInfo holds balancing override information
 type BalancingOverrideInfo struct {
 type BalancingOverrideInfo struct {
 	Until   time.Time
 	Until   time.Time
 	Selects []string
 	Selects []string

+ 1 - 1
functions.go

@@ -24,7 +24,7 @@ func CreateObject(v *Instance, config interface{}) (interface{}, error) {
 //
 //
 // v2ray:api:stable
 // v2ray:api:stable
 func StartInstance(configFormat string, configBytes []byte) (*Instance, error) {
 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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 3 - 3
functions_test.go

@@ -61,7 +61,7 @@ func TestV2RayDial(t *testing.T) {
 	cfgBytes, err := proto.Marshal(config)
 	cfgBytes, err := proto.Marshal(config)
 	common.Must(err)
 	common.Must(err)
 
 
-	server, err := core.StartInstance("protobuf", cfgBytes)
+	server, err := core.StartInstance(core.FormatProtobuf, cfgBytes)
 	common.Must(err)
 	common.Must(err)
 	defer server.Close()
 	defer server.Close()
 
 
@@ -111,7 +111,7 @@ func TestV2RayDialUDPConn(t *testing.T) {
 	cfgBytes, err := proto.Marshal(config)
 	cfgBytes, err := proto.Marshal(config)
 	common.Must(err)
 	common.Must(err)
 
 
-	server, err := core.StartInstance("protobuf", cfgBytes)
+	server, err := core.StartInstance(core.FormatProtobuf, cfgBytes)
 	common.Must(err)
 	common.Must(err)
 	defer server.Close()
 	defer server.Close()
 
 
@@ -178,7 +178,7 @@ func TestV2RayDialUDP(t *testing.T) {
 	cfgBytes, err := proto.Marshal(config)
 	cfgBytes, err := proto.Marshal(config)
 	common.Must(err)
 	common.Must(err)
 
 
-	server, err := core.StartInstance("protobuf", cfgBytes)
+	server, err := core.StartInstance(core.FormatProtobuf, cfgBytes)
 	common.Must(err)
 	common.Must(err)
 	defer server.Close()
 	defer server.Close()
 
 

+ 1 - 0
infra/conf/merge/map.go

@@ -9,6 +9,7 @@ import (
 )
 )
 
 
 // mergeMaps merges source map into target
 // 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) {
 func mergeMaps(target map[string]interface{}, source map[string]interface{}) (err error) {
 	for key, value := range source {
 	for key, value := range source {
 		target[key], err = mergeField(target[key], value)
 		target[key], err = mergeField(target[key], value)

+ 23 - 72
infra/conf/merge/merge.go

@@ -18,95 +18,46 @@ package merge
 import (
 import (
 	"bytes"
 	"bytes"
 	"encoding/json"
 	"encoding/json"
-	"fmt"
-	"io"
 
 
-	"github.com/v2fly/v2ray-core/v4/common/cmdarg"
 	"github.com/v2fly/v2ray-core/v4/infra/conf/serial"
 	"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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	err = applyRules(m)
-	if err != nil {
+	if err = mergeMaps(target, n); err != nil {
 		return nil, err
 		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 {
 	if err != nil {
 		return nil, err
 		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
 package merge_test
 
 
 import (
 import (
-	"encoding/json"
+	"bytes"
 	"reflect"
 	"reflect"
 	"strings"
 	"strings"
 	"testing"
 	"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 {
 	if err != nil {
 		t.Error(err)
 		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 {
 	if err != nil {
 		t.Error(err)
 		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 {
 	if err != nil {
 		t.Error(err)
 		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 {
 	if err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 	assertResult(t, m, expected)
 	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{})
 	e := make(map[string]interface{})
-	err := serial.DecodeJSON(strings.NewReader(expected), &e)
+	err = serial.DecodeJSON(strings.NewReader(expected), &e)
 	if err != nil {
 	if err != nil {
 		t.Error(err)
 		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 priorityKey string = "_priority"
 const tagKey string = "_tag"
 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)
 	err := sortMergeSlices(m)
 	if err != nil {
 	if err != nil {
 		return err
 		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"
 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
 // CmdAPI calls an API in an V2Ray process
 var CmdAPI = &base.Command{
 var CmdAPI = &base.Command{
 	UsageLine: "{{.Exec}} api",
 	UsageLine: "{{.Exec}} api",
-	Short:     "Call V2Ray API",
+	Short:     "call V2Ray API",
 	Long: `{{.Exec}} {{.LongName}} provides tools to manipulate V2Ray via its API.
 	Long: `{{.Exec}} {{.LongName}} provides tools to manipulate V2Ray via its API.
 `,
 `,
 	Commands: []*base.Command{
 	Commands: []*base.Command{
-		cmdRestartLogger,
-		cmdGetStats,
-		cmdQueryStats,
-		cmdSysStats,
+		cmdLog,
+		cmdStats,
 		cmdBalancerCheck,
 		cmdBalancerCheck,
 		cmdBalancerInfo,
 		cmdBalancerInfo,
 		cmdBalancerOverride,
 		cmdBalancerOverride,

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

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

+ 23 - 12
main/commands/all/api/balancer_info.go

@@ -10,6 +10,7 @@ import (
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
 )
 )
 
 
+// TODO: support "-json" flag for json output
 var cmdBalancerInfo = &base.Command{
 var cmdBalancerInfo = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...",
 	UsageLine:   "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...",
@@ -24,10 +25,13 @@ of server config.
 
 
 Arguments:
 Arguments:
 
 
-	-s, -server 
+	-json
+		Use json output.
+
+	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -53,12 +57,17 @@ func executeBalancerInfo(cmd *base.Command, args []string) {
 	sort.Slice(resp.Balancers, func(i, j int) bool {
 	sort.Slice(resp.Balancers, func(i, j int) bool {
 		return resp.Balancers[i].Tag < resp.Balancers[j].Tag
 		return resp.Balancers[i].Tag < resp.Balancers[j].Tag
 	})
 	})
+	if apiJSON {
+		showJSONResponse(resp)
+		return
+	}
 	for _, b := range resp.Balancers {
 	for _, b := range resp.Balancers {
 		showBalancerInfo(b)
 		showBalancerInfo(b)
 	}
 	}
 }
 }
 
 
 func showBalancerInfo(b *routerService.BalancerMsg) {
 func showBalancerInfo(b *routerService.BalancerMsg) {
+	const tableIndent = 4
 	sb := new(strings.Builder)
 	sb := new(strings.Builder)
 	// Balancer
 	// Balancer
 	sb.WriteString(fmt.Sprintf("Balancer: %s\n", b.Tag))
 	sb.WriteString(fmt.Sprintf("Balancer: %s\n", b.Tag))
@@ -71,25 +80,28 @@ func showBalancerInfo(b *routerService.BalancerMsg) {
 	if b.Override != nil {
 	if b.Override != nil {
 		sb.WriteString("  - Selecting Override:\n")
 		sb.WriteString("  - Selecting Override:\n")
 		until := fmt.Sprintf("until: %s", b.Override.Until)
 		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 {
 		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)
 	formats := getColumnFormats(b.Titles)
 	// Selects
 	// Selects
 	sb.WriteString("  - Selects:\n")
 	sb.WriteString("  - Selects:\n")
-	writeRow(sb, 0, b.Titles, formats, "Tag")
+	writeRow(sb, tableIndent, 0, b.Titles, formats)
 	for i, o := range b.Selects {
 	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
 	// Others
 	scnt := len(b.Selects)
 	scnt := len(b.Selects)
 	if len(b.Others) > 0 {
 	if len(b.Others) > 0 {
 		sb.WriteString("  - Others:\n")
 		sb.WriteString("  - Others:\n")
-		writeRow(sb, 0, b.Titles, formats, "Tag")
+		writeRow(sb, tableIndent, 0, b.Titles, formats)
 		for i, o := range b.Others {
 		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())
 	os.Stdout.WriteString(sb.String())
@@ -103,12 +115,12 @@ func getColumnFormats(titles []string) []string {
 	return w
 	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 {
 	if index == 0 {
 		// title line
 		// title line
-		sb.WriteString("        ")
+		sb.WriteString(strings.Repeat(" ", indent+4))
 	} else {
 	} else {
-		sb.WriteString(fmt.Sprintf("    %-4d", index))
+		sb.WriteString(fmt.Sprintf("%s%-4d", strings.Repeat(" ", indent), index))
 	}
 	}
 	for i, v := range values {
 	for i, v := range values {
 		format := "%-14s"
 		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(fmt.Sprintf(format, v))
 	}
 	}
-	sb.WriteString(tag)
 	sb.WriteByte('\n')
 	sb.WriteByte('\n')
 }
 }

+ 17 - 16
main/commands/all/api/balancer_override.go

@@ -10,14 +10,14 @@ import (
 var cmdBalancerOverride = &base.Command{
 var cmdBalancerOverride = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> selectors...",
 	UsageLine:   "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> selectors...",
-	Short:       "balancer select override",
+	Short:       "balancer override",
 	Long: `
 	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" 
 > Make sure you have "RoutingService" set in "config.api.services" 
 of server config.
 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 selectors of the balancer won't apply.
 - The strategy of the balancer stops selecting qualified nodes 
 - The strategy of the balancer stops selecting qualified nodes 
@@ -25,19 +25,20 @@ Once a balancer's selecting is overridden:
 
 
 Arguments:
 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
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -51,13 +52,13 @@ Example:
 func executeBalancerOverride(cmd *base.Command, args []string) {
 func executeBalancerOverride(cmd *base.Command, args []string) {
 	var (
 	var (
 		balancer string
 		balancer string
-		validity int64
+		validity time.Duration
 		remove   bool
 		remove   bool
 	)
 	)
 	cmd.Flag.StringVar(&balancer, "b", "", "")
 	cmd.Flag.StringVar(&balancer, "b", "", "")
 	cmd.Flag.StringVar(&balancer, "balancer", "", "")
 	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, "r", false, "")
 	cmd.Flag.BoolVar(&remove, "remove", false, "")
 	cmd.Flag.BoolVar(&remove, "remove", false, "")
 	setSharedFlags(cmd)
 	setSharedFlags(cmd)
@@ -72,7 +73,7 @@ func executeBalancerOverride(cmd *base.Command, args []string) {
 
 
 	v := int64(0)
 	v := int64(0)
 	if !remove {
 	if !remove {
-		v = int64(time.Duration(validity) * time.Minute)
+		v = int64(validity)
 	}
 	}
 	client := routerService.NewRoutingServiceClient(conn)
 	client := routerService.NewRoutingServiceClient(conn)
 	r := &routerService.OverrideSelectingRequest{
 	r := &routerService.OverrideSelectingRequest{
@@ -82,6 +83,6 @@ func executeBalancerOverride(cmd *base.Command, args []string) {
 	}
 	}
 	_, err := client.OverrideSelecting(ctx, r)
 	_, err := client.OverrideSelecting(ctx, r)
 	if err != nil {
 	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"
 	"fmt"
 
 
 	handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command"
 	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/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands/helpers"
 )
 )
 
 
 var cmdAddInbounds = &base.Command{
 var cmdAddInbounds = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api adi [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
 	UsageLine:   "{{.Exec}} api adi [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
-	Short:       "Add inbounds",
+	Short:       "add inbounds",
 	Long: `
 	Long: `
 Add inbounds to V2Ray.
 Add inbounds to V2Ray.
 
 
 Arguments:
 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
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -34,26 +40,13 @@ Example:
 
 
 func executeAddInbounds(cmd *base.Command, args []string) {
 func executeAddInbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
 	setSharedFlags(cmd)
+	setSharedConfigFlags(cmd)
 	cmd.Flag.Parse(args)
 	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")
 		base.Fatalf("no valid inbound found")
 	}
 	}
 
 
@@ -61,7 +54,7 @@ func executeAddInbounds(cmd *base.Command, args []string) {
 	defer close()
 	defer close()
 
 
 	client := handlerService.NewHandlerServiceClient(conn)
 	client := handlerService.NewHandlerServiceClient(conn)
-	for _, in := range ins {
+	for _, in := range c.InboundConfigs {
 		fmt.Println("adding:", in.Tag)
 		fmt.Println("adding:", in.Tag)
 		i, err := in.Build()
 		i, err := in.Build()
 		if err != nil {
 		if err != nil {
@@ -70,10 +63,9 @@ func executeAddInbounds(cmd *base.Command, args []string) {
 		r := &handlerService.AddInboundRequest{
 		r := &handlerService.AddInboundRequest{
 			Inbound: i,
 			Inbound: i,
 		}
 		}
-		resp, err := client.AddInbound(ctx, r)
+		_, err = client.AddInbound(ctx, r)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to add inbound: %s", err)
 			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"
 	"fmt"
 
 
 	handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command"
 	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/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands/helpers"
 )
 )
 
 
 var cmdRemoveInbounds = &base.Command{
 var cmdRemoveInbounds = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api rmi [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
 	UsageLine:   "{{.Exec}} api rmi [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
-	Short:       "Remove inbounds",
+	Short:       "remove inbounds",
 	Long: `
 	Long: `
 Remove inbounds from V2Ray.
 Remove inbounds from V2Ray.
 
 
 Arguments:
 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
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -33,48 +40,28 @@ Example:
 
 
 func executeRemoveInbounds(cmd *base.Command, args []string) {
 func executeRemoveInbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
 	setSharedFlags(cmd)
+	setSharedConfigFlags(cmd)
 	cmd.Flag.Parse(args)
 	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")
 		base.Fatalf("no inbound to remove")
 	}
 	}
-	fmt.Println("removing inbounds:", tags)
 
 
 	conn, ctx, close := dialAPIServer()
 	conn, ctx, close := dialAPIServer()
 	defer close()
 	defer close()
 
 
 	client := handlerService.NewHandlerServiceClient(conn)
 	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{
 		r := &handlerService.RemoveInboundRequest{
-			Tag: tag,
+			Tag: c.Tag,
 		}
 		}
-		resp, err := client.RemoveInbound(ctx, r)
+		_, err := client.RemoveInbound(ctx, r)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to remove inbound: %s", err)
 			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"
 	"fmt"
 
 
 	handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command"
 	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/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands/helpers"
 )
 )
 
 
 var cmdAddOutbounds = &base.Command{
 var cmdAddOutbounds = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api ado [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
 	UsageLine:   "{{.Exec}} api ado [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
-	Short:       "Add outbounds",
+	Short:       "add outbounds",
 	Long: `
 	Long: `
 Add outbounds to V2Ray.
 Add outbounds to V2Ray.
 
 
 Arguments:
 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
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -34,26 +40,13 @@ Example:
 
 
 func executeAddOutbounds(cmd *base.Command, args []string) {
 func executeAddOutbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
 	setSharedFlags(cmd)
+	setSharedConfigFlags(cmd)
 	cmd.Flag.Parse(args)
 	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")
 		base.Fatalf("no valid outbound found")
 	}
 	}
 
 
@@ -61,7 +54,7 @@ func executeAddOutbounds(cmd *base.Command, args []string) {
 	defer close()
 	defer close()
 
 
 	client := handlerService.NewHandlerServiceClient(conn)
 	client := handlerService.NewHandlerServiceClient(conn)
-	for _, out := range outs {
+	for _, out := range c.OutboundConfigs {
 		fmt.Println("adding:", out.Tag)
 		fmt.Println("adding:", out.Tag)
 		o, err := out.Build()
 		o, err := out.Build()
 		if err != nil {
 		if err != nil {
@@ -70,10 +63,9 @@ func executeAddOutbounds(cmd *base.Command, args []string) {
 		r := &handlerService.AddOutboundRequest{
 		r := &handlerService.AddOutboundRequest{
 			Outbound: o,
 			Outbound: o,
 		}
 		}
-		resp, err := client.AddOutbound(ctx, r)
+		_, err = client.AddOutbound(ctx, r)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to add outbound: %s", err)
 			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"
 	"fmt"
 
 
 	handlerService "github.com/v2fly/v2ray-core/v4/app/proxyman/command"
 	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/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands/helpers"
 )
 )
 
 
 var cmdRemoveOutbounds = &base.Command{
 var cmdRemoveOutbounds = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} api rmo [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
 	UsageLine:   "{{.Exec}} api rmo [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
-	Short:       "Remove outbounds",
+	Short:       "remove outbounds",
 	Long: `
 	Long: `
 Remove outbounds from V2Ray.
 Remove outbounds from V2Ray.
 
 
 Arguments:
 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
 		The API server address. Default 127.0.0.1:8080
 
 
-	-t, -timeout
+	-t, -timeout <seconds>
 		Timeout seconds to call API. Default 3
 		Timeout seconds to call API. Default 3
 
 
 Example:
 Example:
@@ -34,30 +41,12 @@ Example:
 func executeRemoveOutbounds(cmd *base.Command, args []string) {
 func executeRemoveOutbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
 	setSharedFlags(cmd)
 	cmd.Flag.Parse(args)
 	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")
 		base.Fatalf("no outbound to remove")
 	}
 	}
 
 
@@ -65,15 +54,14 @@ func executeRemoveOutbounds(cmd *base.Command, args []string) {
 	defer close()
 	defer close()
 
 
 	client := handlerService.NewHandlerServiceClient(conn)
 	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{
 		r := &handlerService.RemoveOutboundRequest{
-			Tag: tag,
+			Tag: c.Tag,
 		}
 		}
-		resp, err := client.RemoveOutbound(ctx, r)
+		_, err := client.RemoveOutbound(ctx, r)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to remove outbound: %s", err)
 			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"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
-	"reflect"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/proto"
+
+	core "github.com/v2fly/v2ray-core/v4"
 )
 )
 
 
 var (
 var (
-	apiServerAddrPtr string
-	apiTimeout       int
+	apiServerAddrPtr     string
+	apiTimeout           int
+	apiJSON              bool
+	apiConfigFormat      string
+	apiConfigRecursively bool
 )
 )
 
 
 func setSharedFlags(cmd *base.Command) {
 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.StringVar(&apiServerAddrPtr, "server", "127.0.0.1:8080", "")
 	cmd.Flag.IntVar(&apiTimeout, "t", 3, "")
 	cmd.Flag.IntVar(&apiTimeout, "t", 3, "")
 	cmd.Flag.IntVar(&apiTimeout, "timeout", 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()) {
 func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(apiTimeout)*time.Second)
 	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() {
 	close = func() {
 		cancel()
 		cancel()
 		conn.Close()
 		conn.Close()
@@ -39,56 +46,40 @@ func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func())
 	return
 	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)
 	b := new(strings.Builder)
 	e := json.NewEncoder(b)
 	e := json.NewEncoder(b)
-	e.SetIndent("", "  ")
+	e.SetIndent(prefix, indent)
 	e.SetEscapeHTML(false)
 	e.SetEscapeHTML(false)
 	err := e.Encode(m)
 	err := e.Encode(m)
 	if err != nil {
 	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 (
 import (
 	"bytes"
 	"bytes"
 	"encoding/json"
 	"encoding/json"
-	"github.com/pelletier/go-toml"
-	"google.golang.org/protobuf/proto"
 	"os"
 	"os"
 	"strings"
 	"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/infra/conf/serial"
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands/helpers"
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 )
 )
 
 
 var cmdConvert = &base.Command{
 var cmdConvert = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} convert [c1.json] [<url>.json] [dir1] ...",
 	UsageLine:   "{{.Exec}} convert [c1.json] [<url>.json] [dir1] ...",
-	Short:       "Convert config files",
+	Short:       "convert config files",
 	Long: `
 	Long: `
 Convert config files between different formats. Files are merged 
 Convert config files between different formats. Files are merged 
 before convert if multiple assigned.
 before convert if multiple assigned.
 
 
 Arguments:
 Arguments:
 
 
-	-i, -input
+	-i, -input <format>
 		Specify the 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
 		Specify the output format
 		Available values: "json", "toml", "yaml", "protobuf" / "pb"
 		Available values: "json", "toml", "yaml", "protobuf" / "pb"
 		Default: "json"
 		Default: "json"
 
 
 	-r
 	-r
-		Load confdir recursively.
+		Load folders recursively.
 
 
 Examples:
 Examples:
 
 
@@ -61,15 +65,10 @@ var (
 	outputFormat       string
 	outputFormat       string
 	confDirRecursively bool
 	confDirRecursively bool
 )
 )
-var formatExtensions = map[string][]string{
-	"json": {".json", ".jsonc"},
-	"toml": {".toml"},
-	"yaml": {".yaml", ".yml"},
-}
 
 
 func setConfArgs(cmd *base.Command) {
 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, "output", "json", "")
 	cmd.Flag.StringVar(&outputFormat, "o", "json", "")
 	cmd.Flag.StringVar(&outputFormat, "o", "json", "")
 	cmd.Flag.BoolVar(&confDirRecursively, "r", false, "")
 	cmd.Flag.BoolVar(&confDirRecursively, "r", false, "")
@@ -77,37 +76,36 @@ func setConfArgs(cmd *base.Command) {
 func executeConvert(cmd *base.Command, args []string) {
 func executeConvert(cmd *base.Command, args []string) {
 	setConfArgs(cmd)
 	setConfArgs(cmd)
 	cmd.Flag.Parse(args)
 	cmd.Flag.Parse(args)
-	unnamed := cmd.Flag.Args()
 	inputFormat = strings.ToLower(inputFormat)
 	inputFormat = strings.ToLower(inputFormat)
 	outputFormat = strings.ToLower(outputFormat)
 	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 {
 	switch outputFormat {
-	case "json":
+	case core.FormatJSON:
 		out, err = json.Marshal(m)
 		out, err = json.Marshal(m)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to marshal json: %s", err)
 			base.Fatalf("failed to marshal json: %s", err)
 		}
 		}
-	case "toml":
+	case core.FormatTOML:
 		out, err = toml.Marshal(m)
 		out, err = toml.Marshal(m)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to marshal json: %s", err)
 			base.Fatalf("failed to marshal json: %s", err)
 		}
 		}
-	case "yaml":
+	case core.FormatYAML:
 		out, err = yaml.Marshal(m)
 		out, err = yaml.Marshal(m)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to marshal json: %s", err)
 			base.Fatalf("failed to marshal json: %s", err)
 		}
 		}
-	case "pb", "protobuf":
+	case core.FormatProtobuf, core.FormatProtobufShort:
 		data, err := json.Marshal(m)
 		data, err := json.Marshal(m)
 		if err != nil {
 		if err != nil {
 			base.Fatalf("failed to marshal json: %s", err)
 			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 {
 	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: `
 	Long: `
 {{.Exec}} supports different config formats:
 {{.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)
 	* json (.json, .jsonc)
-	  The default loader, multiple config files support.	
+	  The json loader, multiple files support, mergeable.
 
 
 	* toml (.toml)
 	* toml (.toml)
-	  The toml loader, multiple config files support.
+	  The toml loader, multiple files support, mergeable.
 
 
 	* yaml (.yml)
 	* yaml (.yml)
-	  The yaml loader, multiple config files support.
+	  The yaml loader, multiple files support, mergeable.
 
 
 	* protobuf / pb (.pb)
 	* 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.
 The following explains how format loaders behave with examples.
 
 
 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}} run -c c1.json -c c2.json ...
 	{{.Exec}} test -c c1.yaml -c c2.yaml ...
 	{{.Exec}} test -c c1.yaml -c c2.yaml ...
 	{{.Exec}} convert c1.json dir1 ...
 	{{.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, 
 Support of toml and yaml is implemented by converting them to json, 
 both merge and load. So we take json as example here.
 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
 // cmdCert is the tls cert command
 var cmdCert = &base.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",
 	Short:     "Generate TLS certificates",
 	Long: `
 	Long: `
 Generate TLS certificates.
 Generate TLS certificates.
 
 
 Arguments:
 Arguments:
 
 
-	-domain=domain_name 
+	-domain <domain_name>
 		The domain name for the certificate.
 		The domain name for the certificate.
 
 
-	-org=organization 
+	-org <organization>
 		The organization name for the certificate.
 		The organization name for the certificate.
 
 
 	-ca 
 	-ca 
-		Whether this certificate is a CA
+		The certificate is a CA
 
 
 	-json 
 	-json 
-		The output of certificate to JSON
+		To output certificate to JSON
 
 
-	-file 
+	-file <path>
 		The certificate path to save.
 		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
 		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) {
 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.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))
 	opts = append(opts, cert.CommonName(*certCommonName))
 	if len(certDomainNames) > 0 {
 	if len(certDomainNames) > 0 {
 		opts = append(opts, cert.DNSNames(certDomainNames...))
 		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
 // cmdPing is the tls ping command
 var cmdPing = &base.Command{
 var cmdPing = &base.Command{
 	UsageLine: "{{.Exec}} tls ping [-ip <ip>] <domain>",
 	UsageLine: "{{.Exec}} tls ping [-ip <ip>] <domain>",
-	Short:     "Ping the domain with TLS handshake",
+	Short:     "ping the domain with TLS handshake",
 	Long: `
 	Long: `
 Ping the domain with TLS handshake.
 Ping the domain with TLS handshake.
 
 
 Arguments:
 Arguments:
 
 
-	-ip
+	-ip <ip>
 		The IP address of the domain.
 		The IP address of the domain.
 `,
 `,
 }
 }

+ 1 - 1
main/commands/all/uuid.go

@@ -9,7 +9,7 @@ import (
 
 
 var cmdUUID = &base.Command{
 var cmdUUID = &base.Command{
 	UsageLine: "{{.Exec}} uuid",
 	UsageLine: "{{.Exec}} uuid",
-	Short:     "Generate new UUIDs",
+	Short:     "generate new UUIDs",
 	Long: `Generate new UUIDs.
 	Long: `Generate new UUIDs.
 `,
 `,
 	Run: executeUUID,
 	Run: executeUUID,

+ 2 - 2
main/commands/all/verify.go

@@ -9,13 +9,13 @@ import (
 
 
 var cmdVerify = &base.Command{
 var cmdVerify = &base.Command{
 	UsageLine: "{{.Exec}} verify [--sig=sig-file] file",
 	UsageLine: "{{.Exec}} verify [--sig=sig-file] file",
-	Short:     "Verify if a binary is officially signed",
+	Short:     "verify if a binary is officially signed",
 	Long: `
 	Long: `
 Verify if a binary is officially signed.
 Verify if a binary is officially signed.
 
 
 Arguments:
 Arguments:
 
 
-	-sig 
+	-sig <signature_file>
 		The path to the 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
 package commands
 
 
 import (
 import (
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"log"
 	"log"
 	"os"
 	"os"
@@ -20,22 +21,22 @@ import (
 var CmdRun = &base.Command{
 var CmdRun = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} run [-c config.json] [-d dir]",
 	UsageLine:   "{{.Exec}} run [-c config.json] [-d dir]",
-	Short:       "Run V2Ray with config",
+	Short:       "run V2Ray with config",
 	Long: `
 	Long: `
 Run V2Ray with config.
 Run V2Ray with config.
 
 
 Arguments:
 Arguments:
 
 
-	-c, -config
+	-c, -config <file>
 		Config file for V2Ray. Multiple assign is accepted.
 		Config file for V2Ray. Multiple assign is accepted.
 
 
-	-d, -confdir
+	-d, -confdir <dir>
 		A dir with config files. Multiple assign is accepted.
 		A dir with config files. Multiple assign is accepted.
 
 
 	-r
 	-r
 		Load confdir recursively.
 		Load confdir recursively.
 
 
-	-format
+	-format <format>
 		Format of input files. (default "json")
 		Format of input files. (default "json")
 
 
 Examples:
 Examples:
@@ -56,7 +57,7 @@ var (
 )
 )
 
 
 func setConfigFlags(cmd *base.Command) {
 func setConfigFlags(cmd *base.Command) {
-	configFormat = cmd.Flag.String("format", "", "")
+	configFormat = cmd.Flag.String("format", core.FormatAuto, "")
 	configDirRecursively = cmd.Flag.Bool("r", false, "")
 	configDirRecursively = cmd.Flag.Bool("r", false, "")
 
 
 	cmd.Flag.Var(&configFiles, "config", "")
 	cmd.Flag.Var(&configFiles, "config", "")
@@ -69,6 +70,7 @@ func executeRun(cmd *base.Command, args []string) {
 	setConfigFlags(cmd)
 	setConfigFlags(cmd)
 	cmd.Flag.Parse(args)
 	cmd.Flag.Parse(args)
 	printVersion()
 	printVersion()
+	configFiles = getConfigFilePath()
 	server, err := startV2Ray()
 	server, err := startV2Ray()
 	if err != nil {
 	if err != nil {
 		base.Fatalf("Failed to start: %s", err)
 		base.Fatalf("Failed to start: %s", err)
@@ -139,20 +141,8 @@ func readConfDirRecursively(dirPath string, extension []string) cmdarg.Arg {
 	return files
 	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 {
 func getConfigFilePath() cmdarg.Arg {
-	extension, err := getLoaderExtension()
+	extension, err := core.GetLoaderExtensions(*configFormat)
 	if err != nil {
 	if err != nil {
 		base.Fatalf(err.Error())
 		base.Fatalf(err.Error())
 	}
 	}
@@ -190,16 +180,18 @@ func getConfigFilePath() cmdarg.Arg {
 		return cmdarg.Arg{configFile}
 		return cmdarg.Arg{configFile}
 	}
 	}
 
 
-	log.Println("Using config from STDIN")
-	return cmdarg.Arg{"stdin:"}
+	return nil
 }
 }
 
 
 func startV2Ray() (core.Server, error) {
 func startV2Ray() (core.Server, error) {
-	configFiles := getConfigFilePath()
-
-	config, err := core.LoadConfig(*configFormat, configFiles[0], configFiles)
+	config, err := core.LoadConfig(*configFormat, configFiles)
 	if err != nil {
 	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)
 	server, err := core.New(config)

+ 6 - 28
main/commands/test.go

@@ -12,22 +12,22 @@ import (
 var CmdTest = &base.Command{
 var CmdTest = &base.Command{
 	CustomFlags: true,
 	CustomFlags: true,
 	UsageLine:   "{{.Exec}} test [-format=json] [-c config.json] [-d dir]",
 	UsageLine:   "{{.Exec}} test [-format=json] [-c config.json] [-d dir]",
-	Short:       "Test config files",
+	Short:       "test config files",
 	Long: `
 	Long: `
 Test config files, without launching V2Ray server.
 Test config files, without launching V2Ray server.
 
 
 Arguments:
 Arguments:
 
 
-	-c, -config
+	-c, -config <file>
 		Config file for V2Ray. Multiple assign is accepted.
 		Config file for V2Ray. Multiple assign is accepted.
 
 
-	-d, -confdir
+	-d, -confdir <dir>
 		A dir with config files. Multiple assign is accepted.
 		A dir with config files. Multiple assign is accepted.
 
 
 	-r
 	-r
 		Load confdir recursively.
 		Load confdir recursively.
 
 
-	-format
+	-format <format>
 		Format of input files. (default "json")
 		Format of input files. (default "json")
 
 
 Examples:
 Examples:
@@ -44,7 +44,7 @@ func executeTest(cmd *base.Command, args []string) {
 	setConfigFlags(cmd)
 	setConfigFlags(cmd)
 	cmd.Flag.Parse(args)
 	cmd.Flag.Parse(args)
 
 
-	extension, err := getLoaderExtension()
+	extension, err := core.GetLoaderExtensions(*configFormat)
 	if err != nil {
 	if err != nil {
 		base.Fatalf(err.Error())
 		base.Fatalf(err.Error())
 	}
 	}
@@ -59,32 +59,10 @@ func executeTest(cmd *base.Command, args []string) {
 			configFiles = append(configFiles, dirReader(d, extension)...)
 			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()
 	printVersion()
-	_, err = startV2RayTesting()
+	_, err = startV2Ray()
 	if err != nil {
 	if err != nil {
 		base.Fatalf("Test failed: %s", err)
 		base.Fatalf("Test failed: %s", err)
 	}
 	}
 	fmt.Println("Configuration OK.")
 	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
 // CmdVersion prints V2Ray Versions
 var CmdVersion = &base.Command{
 var CmdVersion = &base.Command{
 	UsageLine: "{{.Exec}} version",
 	UsageLine: "{{.Exec}} version",
-	Short:     "Print V2Ray Versions",
+	Short:     "print V2Ray version",
 	Long: `Prints the build information for V2Ray.
 	Long: `Prints the build information for V2Ray.
 `,
 `,
 	Run: executeVersion,
 	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/memconservative"
 	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard"
 	_ "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
 	// commands
 	_ "github.com/v2fly/v2ray-core/v4/main/commands/all"
 	_ "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)
 	os.MkdirAll(covDir, os.ModeDir)
 	randomID := uuid.New()
 	randomID := uuid.New()
 	profile := randomID.String() + ".out"
 	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.Stdin = bytes.NewBuffer(config)
 	proc.Stderr = os.Stderr
 	proc.Stderr = os.Stderr
 	proc.Stdout = os.Stdout
 	proc.Stdout = os.Stdout

+ 1 - 1
testing/scenarios/common_regular.go

@@ -23,7 +23,7 @@ func BuildV2Ray() error {
 
 
 func RunV2RayProtobuf(config []byte) *exec.Cmd {
 func RunV2RayProtobuf(config []byte) *exec.Cmd {
 	genTestBinaryPath()
 	genTestBinaryPath()
-	proc := exec.Command(testBinaryPath, "run", "-config=stdin:", "-format=pb")
+	proc := exec.Command(testBinaryPath, "run", "-format=pb")
 	proc.Stdin = bytes.NewBuffer(config)
 	proc.Stdin = bytes.NewBuffer(config)
 	proc.Stderr = os.Stderr
 	proc.Stderr = os.Stderr
 	proc.Stdout = os.Stdout
 	proc.Stdout = os.Stdout

+ 1 - 1
v2ray_test.go

@@ -85,7 +85,7 @@ func TestV2RayClose(t *testing.T) {
 	cfgBytes, err := proto.Marshal(config)
 	cfgBytes, err := proto.Marshal(config)
 	common.Must(err)
 	common.Must(err)
 
 
-	server, err := StartInstance("protobuf", cfgBytes)
+	server, err := StartInstance(FormatProtobuf, cfgBytes)
 	common.Must(err)
 	common.Must(err)
 	server.Close()
 	server.Close()
 }
 }