Browse Source

fix commands issues (#492)

* fix api commands output

* remove unused code

* fix convert always has -r

* update merge err to locate failed file
Jebbs 4 years ago
parent
commit
ec1694beb1

+ 4 - 3
infra/conf/merge/merge.go

@@ -18,6 +18,7 @@ package merge
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io"
 
 	"github.com/v2fly/v2ray-core/v4/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

+ 51 - 11
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 (
 	"google.golang.org/protobuf/proto"
 )
 
-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 isEmpty(m) {
+		// avoid outputs like `{}`, `{"key":{}}`
+		return
 	}
-	if 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(msg)
+	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
 }

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

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