Browse Source

v5: API commands (rebased from a17fc40651213ec841f8bd5a17f253eedd36f2b6)

Jebbs 5 years ago
parent
commit
0506757caf

+ 0 - 145
commands/all/api.go

@@ -1,145 +0,0 @@
-package all
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"strings"
-	"time"
-
-	"github.com/golang/protobuf/proto"
-	"google.golang.org/grpc"
-
-	logService "github.com/v2fly/v2ray-core/v4/app/log/command"
-	statsService "github.com/v2fly/v2ray-core/v4/app/stats/command"
-	"github.com/v2fly/v2ray-core/v4/commands/base"
-)
-
-// cmdAPI calls an API in an V2Ray process
-var cmdAPI = &base.Command{
-	UsageLine: "{{.Exec}} api [-server 127.0.0.1:8080] <action> <parameter>",
-	Short:     "Call V2Ray API",
-	Long: `
-Call V2Ray API, API calls in this command have a timeout to the server of 3 seconds.
-
-The following methods are currently supported:
-
-	LoggerService.RestartLogger
-	StatsService.GetStats
-	StatsService.QueryStats
-
-Examples:
-
-	{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 LoggerService.RestartLogger '' 
-	{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: "" reset: false'
-	{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.GetStats 'name: "inbound>>>statin>>>traffic>>>downlink" reset: false'
-	{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.GetSysStats ''
-	`,
-}
-
-func init() {
-	cmdAPI.Run = executeAPI // break init loop
-}
-
-var (
-	apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "")
-)
-
-func executeAPI(cmd *base.Command, args []string) {
-	unnamedArgs := cmdAPI.Flag.Args()
-	if len(unnamedArgs) < 2 {
-		base.Fatalf("service name or request not specified.")
-	}
-
-	service, method := getServiceMethod(unnamedArgs[0])
-	handler, found := serivceHandlerMap[strings.ToLower(service)]
-	if !found {
-		base.Fatalf("unknown service: %s", service)
-	}
-
-	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
-	defer cancel()
-
-	conn, err := grpc.DialContext(ctx, *apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
-	if err != nil {
-		base.Fatalf("failed to dial %s", *apiServerAddrPtr)
-	}
-	defer conn.Close()
-
-	response, err := handler(ctx, conn, method, unnamedArgs[1])
-	if err != nil {
-		base.Fatalf("failed to call service %s: %s", unnamedArgs[0], err)
-	}
-
-	fmt.Println(response)
-}
-
-func getServiceMethod(s string) (string, string) {
-	ss := strings.Split(s, ".")
-	service := ss[0]
-	var method string
-	if len(ss) > 1 {
-		method = ss[1]
-	}
-	return service, method
-}
-
-type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error)
-
-var serivceHandlerMap = map[string]serviceHandler{
-	"statsservice":  callStatsService,
-	"loggerservice": callLogService,
-}
-
-func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
-	client := logService.NewLoggerServiceClient(conn)
-
-	switch strings.ToLower(method) {
-	case "restartlogger":
-		r := &logService.RestartLoggerRequest{}
-		resp, err := client.RestartLogger(ctx, r)
-		if err != nil {
-			return "", err
-		}
-		return proto.MarshalTextString(resp), nil
-	default:
-		return "", errors.New("Unknown method: " + method)
-	}
-}
-
-func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
-	client := statsService.NewStatsServiceClient(conn)
-
-	switch strings.ToLower(method) {
-	case "getstats":
-		r := &statsService.GetStatsRequest{}
-		if err := proto.UnmarshalText(request, r); err != nil {
-			return "", err
-		}
-		resp, err := client.GetStats(ctx, r)
-		if err != nil {
-			return "", err
-		}
-		return proto.MarshalTextString(resp), nil
-	case "querystats":
-		r := &statsService.QueryStatsRequest{}
-		if err := proto.UnmarshalText(request, r); err != nil {
-			return "", err
-		}
-		resp, err := client.QueryStats(ctx, r)
-		if err != nil {
-			return "", err
-		}
-		return proto.MarshalTextString(resp), nil
-	case "getsysstats":
-		// SysStatsRequest is an empty message
-		r := &statsService.SysStatsRequest{}
-		resp, err := client.GetSysStats(ctx, r)
-		if err != nil {
-			return "", err
-		}
-		return proto.MarshalTextString(resp), nil
-	default:
-		return "", errors.New("Unknown method: " + method)
-	}
-}

+ 23 - 0
commands/all/api/api.go

@@ -0,0 +1,23 @@
+package api
+
+import (
+	"v2ray.com/core/commands/base"
+)
+
+// CmdAPI calls an API in an V2Ray process
+var CmdAPI = &base.Command{
+	UsageLine: "{{.Exec}} api",
+	Short:     "Call V2Ray API",
+	Long: `{{.Exec}} {{.LongName}} provides tools to manipulate V2Ray via its API.
+`,
+	Commands: []*base.Command{
+		cmdRestartLogger,
+		cmdGetStats,
+		cmdQueryStats,
+		cmdSysStats,
+		cmdAddInbounds,
+		cmdAddOutbounds,
+		cmdRemoveInbounds,
+		cmdRemoveOutbounds,
+	},
+}

+ 78 - 0
commands/all/api/inbounds_add.go

@@ -0,0 +1,78 @@
+package api
+
+import (
+	"fmt"
+
+	handlerService "v2ray.com/core/app/proxyman/command"
+	"v2ray.com/core/commands/base"
+	"v2ray.com/core/infra/conf"
+	"v2ray.com/core/infra/conf/serial"
+)
+
+var cmdAddInbounds = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api adi [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
+	Short:       "Add inbounds",
+	Long: `
+Add inbounds to V2Ray.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
+`,
+	Run: executeAddInbounds,
+}
+
+func executeAddInbounds(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+	unnamedArgs := cmd.Flag.Args()
+	if len(unnamedArgs) == 0 {
+		fmt.Println("reading from stdin:")
+		unnamedArgs = []string{"stdin:"}
+	}
+
+	ins := make([]conf.InboundDetourConfig, 0)
+	for _, arg := range unnamedArgs {
+		r, err := loadArg(arg)
+		if err != nil {
+			base.Fatalf("failed to load %s: %s", arg, err)
+		}
+		conf, err := serial.DecodeJSONConfig(r)
+		if err != nil {
+			base.Fatalf("failed to decode %s: %s", arg, err)
+		}
+		ins = append(ins, conf.InboundConfigs...)
+	}
+	if len(ins) == 0 {
+		base.Fatalf("no valid inbound found")
+	}
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := handlerService.NewHandlerServiceClient(conn)
+	for _, in := range ins {
+		fmt.Println("adding:", in.Tag)
+		i, err := in.Build()
+		if err != nil {
+			base.Fatalf("failed to build conf: %s", err)
+		}
+		r := &handlerService.AddInboundRequest{
+			Inbound: i,
+		}
+		resp, err := client.AddInbound(ctx, r)
+		if err != nil {
+			base.Fatalf("failed to add inbound: %s", err)
+		}
+		showResponese(resp)
+	}
+}

+ 79 - 0
commands/all/api/inbounds_remove.go

@@ -0,0 +1,79 @@
+package api
+
+import (
+	"fmt"
+
+	handlerService "v2ray.com/core/app/proxyman/command"
+	"v2ray.com/core/commands/base"
+	"v2ray.com/core/infra/conf/serial"
+)
+
+var cmdRemoveInbounds = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api rmi [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
+	Short:       "Remove inbounds",
+	Long: `
+Remove inbounds from V2Ray.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
+`,
+	Run: executeRemoveInbounds,
+}
+
+func executeRemoveInbounds(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+	unnamedArgs := cmd.Flag.Args()
+	if len(unnamedArgs) == 0 {
+		fmt.Println("reading from stdin:")
+		unnamedArgs = []string{"stdin:"}
+	}
+
+	tags := make([]string, 0)
+	for _, arg := range unnamedArgs {
+		if r, err := loadArg(arg); err == nil {
+			conf, err := serial.DecodeJSONConfig(r)
+			if err != nil {
+				base.Fatalf("failed to decode %s: %s", arg, err)
+			}
+			ins := conf.InboundConfigs
+			for _, i := range ins {
+				tags = append(tags, i.Tag)
+			}
+		} else {
+			// take request as tag
+			tags = append(tags, arg)
+		}
+	}
+
+	if len(tags) == 0 {
+		base.Fatalf("no inbound to remove")
+	}
+	fmt.Println("removing inbounds:", tags)
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := handlerService.NewHandlerServiceClient(conn)
+	for _, tag := range tags {
+		fmt.Println("removing:", tag)
+		r := &handlerService.RemoveInboundRequest{
+			Tag: tag,
+		}
+		resp, err := client.RemoveInbound(ctx, r)
+		if err != nil {
+			base.Fatalf("failed to remove inbound: %s", err)
+		}
+		showResponese(resp)
+	}
+}

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

@@ -0,0 +1,40 @@
+package api
+
+import (
+	logService "v2ray.com/core/app/log/command"
+	"v2ray.com/core/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)
+}

+ 78 - 0
commands/all/api/outbounds_add.go

@@ -0,0 +1,78 @@
+package api
+
+import (
+	"fmt"
+
+	handlerService "v2ray.com/core/app/proxyman/command"
+	"v2ray.com/core/commands/base"
+	"v2ray.com/core/infra/conf"
+	"v2ray.com/core/infra/conf/serial"
+)
+
+var cmdAddOutbounds = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api ado [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
+	Short:       "Add outbounds",
+	Long: `
+Add outbounds to V2Ray.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
+`,
+	Run: executeAddOutbounds,
+}
+
+func executeAddOutbounds(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+	unnamedArgs := cmd.Flag.Args()
+	if len(unnamedArgs) == 0 {
+		fmt.Println("Reading from STDIN")
+		unnamedArgs = []string{"stdin:"}
+	}
+
+	outs := make([]conf.OutboundDetourConfig, 0)
+	for _, arg := range unnamedArgs {
+		r, err := loadArg(arg)
+		if err != nil {
+			base.Fatalf("failed to load %s: %s", arg, err)
+		}
+		conf, err := serial.DecodeJSONConfig(r)
+		if err != nil {
+			base.Fatalf("failed to decode %s: %s", arg, err)
+		}
+		outs = append(outs, conf.OutboundConfigs...)
+	}
+	if len(outs) == 0 {
+		base.Fatalf("no valid outbound found")
+	}
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := handlerService.NewHandlerServiceClient(conn)
+	for _, out := range outs {
+		fmt.Println("adding:", out.Tag)
+		o, err := out.Build()
+		if err != nil {
+			base.Fatalf("failed to build conf: %s", err)
+		}
+		r := &handlerService.AddOutboundRequest{
+			Outbound: o,
+		}
+		resp, err := client.AddOutbound(ctx, r)
+		if err != nil {
+			base.Fatalf("failed to add outbound: %s", err)
+		}
+		showResponese(resp)
+	}
+}

+ 78 - 0
commands/all/api/outbounds_remove.go

@@ -0,0 +1,78 @@
+package api
+
+import (
+	"fmt"
+
+	handlerService "v2ray.com/core/app/proxyman/command"
+	"v2ray.com/core/commands/base"
+	"v2ray.com/core/infra/conf/serial"
+)
+
+var cmdRemoveOutbounds = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api rmo [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
+	Short:       "Remove outbounds",
+	Long: `
+Remove outbounds from V2Ray.
+
+Arguments:
+
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+
+Example:
+
+    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
+`,
+	Run: executeRemoveOutbounds,
+}
+
+func executeRemoveOutbounds(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	cmd.Flag.Parse(args)
+	unnamedArgs := cmd.Flag.Args()
+	if len(unnamedArgs) == 0 {
+		fmt.Println("reading from stdin:")
+		unnamedArgs = []string{"stdin:"}
+	}
+
+	tags := make([]string, 0)
+	for _, arg := range unnamedArgs {
+		if r, err := loadArg(arg); err == nil {
+			conf, err := serial.DecodeJSONConfig(r)
+			if err != nil {
+				base.Fatalf("failed to decode %s: %s", arg, err)
+			}
+			outs := conf.OutboundConfigs
+			for _, o := range outs {
+				tags = append(tags, o.Tag)
+			}
+		} else {
+			// take request as tag
+			tags = append(tags, arg)
+		}
+	}
+
+	if len(tags) == 0 {
+		base.Fatalf("no outbound to remove")
+	}
+
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := handlerService.NewHandlerServiceClient(conn)
+	for _, tag := range tags {
+		fmt.Println("removing:", tag)
+		r := &handlerService.RemoveOutboundRequest{
+			Tag: tag,
+		}
+		resp, err := client.RemoveOutbound(ctx, r)
+		if err != nil {
+			base.Fatalf("failed to remove outbound: %s", err)
+		}
+		showResponese(resp)
+	}
+}

+ 118 - 0
commands/all/api/shared.go

@@ -0,0 +1,118 @@
+package api
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+	"time"
+
+	"google.golang.org/grpc"
+	"google.golang.org/protobuf/proto"
+	"v2ray.com/core/commands/base"
+	"v2ray.com/core/common/buf"
+)
+
+type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, cmd *base.Command, args []string) string
+
+var (
+	apiServerAddrPtr string
+	apiTimeout       int
+)
+
+func setSharedFlags(cmd *base.Command) {
+	cmd.Flag.StringVar(&apiServerAddrPtr, "s", "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, "timeout", 3, "")
+}
+
+func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(apiTimeout)*time.Second)
+	conn, err := grpc.DialContext(ctx, apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
+	if err != nil {
+		base.Fatalf("failed to dial %s", apiServerAddrPtr)
+	}
+	close = func() {
+		cancel()
+		conn.Close()
+	}
+	return
+}
+
+// loadArg loads one arg, maybe an remote url, or local file path
+func loadArg(arg string) (out io.Reader, err error) {
+	var data []byte
+	switch {
+	case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
+		data, err = fetchHTTPContent(arg)
+
+	case arg == "stdin:":
+		data, err = ioutil.ReadAll(os.Stdin)
+
+	default:
+		data, err = ioutil.ReadFile(arg)
+	}
+
+	if err != nil {
+		return
+	}
+	out = bytes.NewBuffer(data)
+	return
+}
+
+// fetchHTTPContent dials https for remote content
+func fetchHTTPContent(target string) ([]byte, error) {
+	parsedTarget, err := url.Parse(target)
+	if err != nil {
+		return nil, err
+	}
+
+	if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
+		return nil, fmt.Errorf("invalid scheme: %s", parsedTarget.Scheme)
+	}
+
+	client := &http.Client{
+		Timeout: 30 * time.Second,
+	}
+	resp, err := client.Do(&http.Request{
+		Method: "GET",
+		URL:    parsedTarget,
+		Close:  true,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("failed to dial to %s", target)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
+	}
+
+	content, err := buf.ReadAllToBytes(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read HTTP response")
+	}
+
+	return content, nil
+}
+
+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 == "" {
+		return
+	}
+	fmt.Println(msg)
+}

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

@@ -0,0 +1,55 @@
+package api
+
+import (
+	statsService "v2ray.com/core/app/stats/command"
+	"v2ray.com/core/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)
+}

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

@@ -0,0 +1,55 @@
+package api
+
+import (
+	statsService "v2ray.com/core/app/stats/command"
+	"v2ray.com/core/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)
+}

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

@@ -0,0 +1,40 @@
+package api
+
+import (
+	statsService "v2ray.com/core/app/stats/command"
+	"v2ray.com/core/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)
+}

+ 7 - 3
commands/all/commands.go

@@ -1,16 +1,20 @@
 package all
 
-import "github.com/v2fly/v2ray-core/v4/commands/base"
+import (
+	"github.com/v2fly/v2ray-core/v4/commands/all/api"
+	"github.com/v2fly/v2ray-core/v4/commands/all/tls"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+)
 
 // go:generate go run v2ray.com/core/common/errors/errorgen
 
 func init() {
 	base.RootCommand.Commands = append(
 		base.RootCommand.Commands,
-		cmdAPI,
+		api.CmdAPI,
 		cmdConvert,
 		cmdLove,
-		cmdTLS,
+		tls.CmdTLS,
 		cmdUUID,
 		cmdVerify,
 		cmdMerge,

+ 0 - 18
commands/all/tls.go

@@ -1,18 +0,0 @@
-package all
-
-import (
-	"github.com/v2fly/v2ray-core/v4/commands/all/tlscmd"
-	"github.com/v2fly/v2ray-core/v4/commands/base"
-)
-
-var cmdTLS = &base.Command{
-	UsageLine: "{{.Exec}} tls",
-	Short:     "TLS tools",
-	Long: `{{.Exec}} tls provides tools for TLS.
-	`,
-
-	Commands: []*base.Command{
-		tlscmd.CmdCert,
-		tlscmd.CmdPing,
-	},
-}

+ 11 - 11
commands/all/tlscmd/cert.go → commands/all/tls/cert.go

@@ -1,4 +1,4 @@
-package tlscmd
+package tls
 
 import (
 	"context"
@@ -14,8 +14,8 @@ import (
 	"github.com/v2fly/v2ray-core/v4/common/task"
 )
 
-// CmdCert is the tls cert command
-var CmdCert = &base.Command{
+// cmdCert is the tls cert command
+var cmdCert = &base.Command{
 	UsageLine: "{{.Exec}} tls cert [--ca] [--domain=v2ray.com] [--expire=240h]",
 	Short:     "Generate TLS certificates",
 	Long: `
@@ -44,22 +44,22 @@ Arguments:
 }
 
 func init() {
-	CmdCert.Run = executeCert // break init loop
+	cmdCert.Run = executeCert // break init loop
 }
 
 var (
 	certDomainNames stringList
 	_               = func() bool {
-		CmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate")
+		cmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate")
 		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", "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.")
 )
 
 func executeCert(cmd *base.Command, args []string) {

+ 7 - 7
commands/all/tlscmd/ping.go → commands/all/tls/ping.go

@@ -1,4 +1,4 @@
-package tlscmd
+package tls
 
 import (
 	"crypto/tls"
@@ -11,8 +11,8 @@ import (
 	v2tls "github.com/v2fly/v2ray-core/v4/transport/internet/tls"
 )
 
-// CmdPing is the tls ping command
-var CmdPing = &base.Command{
+// cmdPing is the tls ping command
+var cmdPing = &base.Command{
 	UsageLine: "{{.Exec}} tls ping [-ip <ip>] <domain>",
 	Short:     "Ping the domain with TLS handshake",
 	Long: `
@@ -26,19 +26,19 @@ Arguments:
 }
 
 func init() {
-	CmdPing.Run = executePing // break init loop
+	cmdPing.Run = executePing // break init loop
 }
 
 var (
-	pingIPStr = CmdPing.Flag.String("ip", "", "")
+	pingIPStr = cmdPing.Flag.String("ip", "", "")
 )
 
 func executePing(cmd *base.Command, args []string) {
-	if CmdPing.Flag.NArg() < 1 {
+	if cmdPing.Flag.NArg() < 1 {
 		base.Fatalf("domain not specified")
 	}
 
-	domain := CmdPing.Flag.Arg(0)
+	domain := cmdPing.Flag.Arg(0)
 	fmt.Println("Tls ping: ", domain)
 
 	var ip net.IP

+ 18 - 0
commands/all/tls/tls.go

@@ -0,0 +1,18 @@
+package tls
+
+import (
+	"v2ray.com/core/commands/base"
+)
+
+// CmdTLS holds all tls sub commands
+var CmdTLS = &base.Command{
+	UsageLine: "{{.Exec}} tls",
+	Short:     "TLS tools",
+	Long: `{{.Exec}} {{.LongName}} provides tools for TLS.
+	`,
+
+	Commands: []*base.Command{
+		cmdCert,
+		cmdPing,
+	},
+}

+ 16 - 7
commands/base/command.go

@@ -22,13 +22,19 @@ type Command struct {
 	Run func(cmd *Command, args []string)
 
 	// UsageLine is the one-line usage message.
-	// The words between "go" and the first flag or argument in the line are taken to be the command name.
+	// The words between the first word (the "executable name") and the first flag or argument in the line are taken to be the command name.
+	//
+	// UsageLine supports go template syntax. It's recommended to use "{{.Exec}}" instead of hardcoding name
 	UsageLine string
 
 	// Short is the short description shown in the 'go help' output.
+	//
+	// Note: Short does not support go template syntax.
 	Short string
 
 	// Long is the long message shown in the 'go help <this-command>' output.
+	//
+	// Long supports go template syntax. It's recommended to use "{{.Exec}}", "{{.LongName}}" instead of hardcoding strings
 	Long string
 
 	// Flag is a set of flags specific to this command.
@@ -44,16 +50,18 @@ type Command struct {
 	Commands []*Command
 }
 
-// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument,
+// LongName returns the command's long name: all the words in the usage line between first word (e.g. "v2ray") and a flag or argument,
 func (c *Command) LongName() string {
 	name := c.UsageLine
 	if i := strings.Index(name, " ["); i >= 0 {
-		name = name[:i]
+		name = strings.TrimSpace(name[:i])
 	}
-	if name == CommandEnv.Exec {
-		return ""
+	if i := strings.Index(name, " "); i >= 0 {
+		name = name[i+1:]
+	} else {
+		name = ""
 	}
-	return strings.TrimPrefix(name, CommandEnv.Exec+" ")
+	return strings.TrimSpace(name)
 }
 
 // Name returns the command's short name: the last word in the usage line before a flag or argument.
@@ -62,11 +70,12 @@ func (c *Command) Name() string {
 	if i := strings.LastIndex(name, " "); i >= 0 {
 		name = name[i+1:]
 	}
-	return name
+	return strings.TrimSpace(name)
 }
 
 // Usage prints usage of the Command
 func (c *Command) Usage() {
+	buildCommandText(c)
 	fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine)
 	fmt.Fprintf(os.Stderr, "Run '%s help %s' for details.\n", CommandEnv.Exec, c.LongName())
 	SetExitStatus(2)

+ 1 - 1
commands/base/execute.go

@@ -14,7 +14,6 @@ import (
 
 // Execute excute the commands
 func Execute() {
-	buildCommandsText(RootCommand)
 	flag.Parse()
 	args := flag.Args()
 	if len(args) < 1 {
@@ -61,6 +60,7 @@ BigCmdLoop:
 				args = cmd.Flag.Args()
 			}
 
+			buildCommandText(cmd)
 			cmd.Run(cmd, args)
 			Exit()
 			return

+ 9 - 11
commands/base/help.go

@@ -41,6 +41,7 @@ Args:
 	if len(cmd.Commands) > 0 {
 		PrintUsage(os.Stdout, cmd)
 	} else {
+		buildCommandText(cmd)
 		tmpl(os.Stdout, helpTemplate, makeTmplData(cmd))
 	}
 }
@@ -119,24 +120,21 @@ func width(width int, value string) string {
 
 // PrintUsage prints usage of cmd to w
 func PrintUsage(w io.Writer, cmd *Command) {
+	buildCommandText(cmd)
 	bw := bufio.NewWriter(w)
 	tmpl(bw, usageTemplate, makeTmplData(cmd))
 	bw.Flush()
 }
 
-// buildCommandsText build text of command and its children as template
-func buildCommandsText(cmd *Command) {
-	buildCommandText(cmd)
-	for _, cmd := range cmd.Commands {
-		buildCommandsText(cmd)
-	}
-}
-
 // buildCommandText build command text as template
 func buildCommandText(cmd *Command) {
-	cmd.UsageLine = buildText(cmd.UsageLine, makeTmplData(cmd))
-	cmd.Short = buildText(cmd.Short, makeTmplData(cmd))
-	cmd.Long = buildText(cmd.Long, makeTmplData(cmd))
+	data := makeTmplData(cmd)
+	cmd.UsageLine = buildText(cmd.UsageLine, data)
+	// DO NOT SUPPORT ".Short":
+	// - It's not necessary
+	// - Or, we have to build text for all sub commands of current command, like "v2ray help api"
+	// cmd.Short = buildText(cmd.Short, data)
+	cmd.Long = buildText(cmd.Long, data)
 }
 
 func buildText(text string, data interface{}) string {