From a5e24544a2bd35c5a142e8b008a48916c293d18c Mon Sep 17 00:00:00 2001 From: Jebbs Date: Sun, 20 Dec 2020 13:35:52 +0800 Subject: [PATCH] fix commands issues (#492) * fix api commands output * remove unused code * fix convert always has -r * update merge err to locate failed file --- infra/conf/merge/merge.go | 7 +- main/commands/all/api/shared.go | 64 +++++++++--- main/commands/all/api/shared_test.go | 140 +++++++++++++++++++++++++++ main/commands/all/convert.go | 2 +- 4 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 main/commands/all/api/shared_test.go diff --git a/infra/conf/merge/merge.go b/infra/conf/merge/merge.go index cdc39097d..2989fa200 100644 --- a/infra/conf/merge/merge.go +++ b/infra/conf/merge/merge.go @@ -18,6 +18,7 @@ package merge import ( "bytes" "encoding/json" + "fmt" "io" "v2ray.com/core/common/cmdarg" @@ -73,14 +74,14 @@ func loadFiles(args []string) (map[string]interface{}, error) { for _, arg := range args { r, err := cmdarg.LoadArg(arg) if err != nil { - return nil, err + return nil, fmt.Errorf("fail to load %s: %s", arg, err) } m, err := decode(r) if err != nil { - return nil, err + return nil, fmt.Errorf("fail to decode %s: %s", arg, err) } if err = mergeMaps(c, m); err != nil { - return nil, err + return nil, fmt.Errorf("fail to merge %s: %s", arg, err) } } return c, nil diff --git a/main/commands/all/api/shared.go b/main/commands/all/api/shared.go index 6217d55e8..cdc3f10c3 100644 --- a/main/commands/all/api/shared.go +++ b/main/commands/all/api/shared.go @@ -2,7 +2,10 @@ package api import ( "context" + "encoding/json" "fmt" + "os" + "reflect" "strings" "time" @@ -11,8 +14,6 @@ import ( "v2ray.com/core/main/commands/base" ) -type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, cmd *base.Command, args []string) string - var ( apiServerAddrPtr string apiTimeout int @@ -39,16 +40,55 @@ func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) } func showResponese(m proto.Message) { - msg := "" - bs, err := proto.Marshal(m) - if err != nil { - msg = err.Error() - } else { - msg = string(bs) - msg = strings.TrimSpace(msg) - } - if msg == "" { + if isEmpty(m) { + // avoid outputs like `{}`, `{"key":{}}` return } - fmt.Println(msg) + b := new(strings.Builder) + e := json.NewEncoder(b) + e.SetIndent("", " ") + e.SetEscapeHTML(false) + err := e.Encode(m) + if err != nil { + fmt.Fprintf(os.Stdout, "%v\n", m) + base.Fatalf("error encode json: %s", err) + return + } + fmt.Println(strings.TrimSpace(b.String())) +} + +// 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 + } + } + return true } diff --git a/main/commands/all/api/shared_test.go b/main/commands/all/api/shared_test.go new file mode 100644 index 000000000..cae9f07c1 --- /dev/null +++ b/main/commands/all/api/shared_test.go @@ -0,0 +1,140 @@ +package api + +import ( + "testing" + + statsService "v2ray.com/core/app/stats/command" +) + +func TestEmptyResponese_0(t *testing.T) { + r := &statsService.QueryStatsResponse{ + Stat: []*statsService.Stat{ + { + Name: "1>>2", + Value: 1, + }, + { + Name: "1>>2>>3", + Value: 2, + }, + }, + } + assert(t, isEmpty(r), false) +} + +func TestEmptyResponese_1(t *testing.T) { + r := (*statsService.QueryStatsResponse)(nil) + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_2(t *testing.T) { + r := &statsService.QueryStatsResponse{ + Stat: nil, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_3(t *testing.T) { + r := &statsService.QueryStatsResponse{ + Stat: []*statsService.Stat{}, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_4(t *testing.T) { + r := &statsService.QueryStatsResponse{ + Stat: []*statsService.Stat{ + { + Name: "", + Value: 0, + }, + }, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_5(t *testing.T) { + type test struct { + Value *statsService.QueryStatsResponse + } + r := &test{ + Value: &statsService.QueryStatsResponse{ + Stat: []*statsService.Stat{ + { + Name: "", + }, + }, + }, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_6(t *testing.T) { + type test struct { + Value *statsService.QueryStatsResponse + } + r := &test{ + Value: &statsService.QueryStatsResponse{ + Stat: []*statsService.Stat{ + { + Value: 1, + }, + }, + }, + } + assert(t, isEmpty(r), false) +} + +func TestEmptyResponese_7(t *testing.T) { + type test struct { + Value *int + } + v := 1 + r := &test{ + Value: &v, + } + assert(t, isEmpty(r), false) +} + +func TestEmptyResponese_8(t *testing.T) { + type test struct { + Value *int + } + v := 0 + r := &test{ + Value: &v, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_9(t *testing.T) { + assert(t, isEmpty(0), true) +} + +func TestEmptyResponese_10(t *testing.T) { + assert(t, isEmpty(1), false) +} + +func TestEmptyResponese_11(t *testing.T) { + r := []*statsService.Stat{ + { + Name: "", + }, + } + assert(t, isEmpty(r), true) +} + +func TestEmptyResponese_12(t *testing.T) { + r := []*statsService.Stat{ + { + Value: 1, + }, + } + assert(t, isEmpty(r), false) +} + +func assert(t *testing.T, value, expected bool) { + if value != expected { + t.Fatalf("Expected: %v, actual: %v", expected, value) + } +} diff --git a/main/commands/all/convert.go b/main/commands/all/convert.go index 7d416ef1b..392e7de68 100644 --- a/main/commands/all/convert.go +++ b/main/commands/all/convert.go @@ -72,7 +72,7 @@ func setConfArgs(cmd *base.Command) { cmd.Flag.StringVar(&inputFormat, "i", "json", "") cmd.Flag.StringVar(&outputFormat, "output", "json", "") cmd.Flag.StringVar(&outputFormat, "o", "json", "") - cmd.Flag.BoolVar(&confDirRecursively, "r", true, "") + cmd.Flag.BoolVar(&confDirRecursively, "r", false, "") } func executeConvert(cmd *base.Command, args []string) { setConfArgs(cmd)